Geocoding server with web interface

In a previous tutorial, I explained the process: Set up OpenStreetMap (OSM) tile server. This tutorial is going to show you how to set up Nominatim Geocoding server with web interface. Nominatim provides search functionality for OpenStreetMap, so if a visitor enters an address in a search box, the latitude/longitude location for that address will be returned. Refer to the alternative guide if you wish to set up both online and offline geocoding service.

1. Build Nominatim from source

Install dependency packages to build Nominatim.

sudo apt-get update
sudo apt install build-essential cmake g++ libboost-dev libboost-system-dev libboost-filesystem-dev libexpat1-dev zlib1g-dev libbz2-dev libpq-dev libproj-dev php php-pgsql php-intl php-cgi phpunit php-codesniffer python3-setuptools python3-dev python3-pip python3-psycopg2 python3-tidylib python3-behave python3-pytest pylint git clang-tidy postgresql-server-dev-13

The installtion is tested on Debian 11 and Ubuntu 20.04. Minor modifications may be needed for other systems.

Create the nominatim user. (No need to create a password for this user.)

sudo useradd -d /srv/nominatim -s /bin/bash -m nominatim

Grant permissions to your own user account. Repalce username with your real username.

sudo setfacl -R -m u:username:rwx /srv/nominatim/

Change to the /srv/nominatim/ directory.

cd /srv/nominatim/

Download Nominatim from the official website.

wget https://nominatim.org/release/Nominatim-3.5.2.tar.bz2

Extract the tarball.

tar xvf Nominatim-3.5.2.tar.bz2

Create the build directory.

mkdir build

Change to this directory and configure the build environment.

cd build
cmake /srv/nominatim/Nominatim-3.5.2

Compile the source code.

make

2. Configure Nominatim

The default configuration file for Nominatim is /srv/nominatim/build/settings/settings.php. We can create a local.php file and add our modifications there.

sudo vi /srv/nominatim/build/settings/local.php

Add the following lines in the file.
<?php
 @define('CONST_Website_BaseURL', '/nominatim/');
 @define('CONST_Default_Lat', 32);
 @define('CONST_Default_Lon', 117);
 @define('CONST_Default_Zoom', 7);
 @define('CONST_Map_Tile_URL', 'https://t.umd.me.uk/osm/{z}/{x}/{y}.png');

?>
        
The above configuration defines You can also take a look at the /srv/nominatim/build/settings/settings.php file and add your own customizations if the need arises. For example, if you are going to import a large dataset (Europe, North America, planet, etc.), it’s a good practice to enable flat node storage of node locations, so node coordinates will be stored in a simple file instead of the database, saving you import time and disk storage.

@define('CONST_Osm2pgsql_Flatnode_File', '/srv/nominatim/flatnode.file');

3. Import OSM database

Download Wikipedia importance dump file, which will improve the quality of the Nomiatim search results.

cd /srv/nominatim/Nominatim-3.5.2/data
wget https://www.nominatim.org/data/wikimedia-importance.sql.gz

Download US and UK postcodes data.

wget https://www.nominatim.org/data/us_postcode_data.sql.gz
wget https://www.nominatim.org/data/gb_postcode_data.sql.gz

Download country code data file.

wget -O country_osm_grid.sql.gz https://www.nominatim.org/data/country_grid.sql.gz

Then you need to download an OSM file and import it to PostgreSQL. You can go to http://download.geofabrik.de to download the extract you need. You can also use the PBF file during the tile server setup process.

Create the www-data user in PostgreSQL, so the web server will have read-only access to the database.

sudo -u postgres -i createuser www-data

Grant permission to the postgres user.

sudo setfacl -R -m u:postgres:rwx /srv/nominatim/

Switch to the postgres user.

sudo -u postgres -i

And run the following command to import OSM extracts to PostgreSQL.

cd /srv/nominatim/build/
/srv/nominatim/build/utils/setup.php --osm-file /var/lib/postgresql/anhui-latest.osm.pbf --all 2>&1 | tee setup.log

It will automatically create the nominatim database in PostgreSQL and import OSM extracts with osm2pgsql.

Sample output:

Processing: Node(25848k 241.6k/s) Way(2960k 18.85k/s) Relation (22670 1133.50/s) parse time: 284s
Node stats: total(25848659), max(13039901645) in 107s
Way stats: total(2960045), max(1418977325) in 157s
Relation stats: total(25417), max(19412309) in 20s
Stopping table: planet_osm_nodes
Stopped table: planet_osm_nodes in 0s
Stopping table: planet_osm_ways
Stopped table: planet_osm_ways in 0s
Stopping table: planet_osm_rels
Building index on table: planet_osm_rels
Stopped table: planet_osm_rels in 1s
Osm2pgsql took 287s overall
node cache: stored: 25848659(100.00%), storage efficiency: 53.25% (dense blocks: 1170, sparse nodes: 19478552), 
    hit rate: 100.00%
2025-08-03 22:27:21 == Create Functions
2025-08-03 22:27:22 == Crate Tables
2025-08-03 22:27:23 == Create Functions
2025-08-03 22:27:23 == Create Tables
2025-08-03 22:27:23 == Create Partition Tables
2025-08-03 22:27:34 == Create Partition Functions
2025-08-03 22:27:35 == Importing Wikipedia articles and redirects
2025-08-03 22:35:43 == Drop old Data …………………….
2025-08-03 22:35:47 == Loading word list
count
-------------
162480
(1 row)
count
-------------
26438
(1 row)
2025-08-03 22:36:47 == Load Data
2025-08-03 22:41:43 == Reanalysing database
Latest data imported from 2025-07-31T18:35:09Z.
2025-08-03 22:43:50 == Calculate Postcodes
        

After importing the database, the indexing process will begin. There are 30 ranks in total.

Once it’s finished, run the following command to verify.

/srv/nominatim/build/utils/check_import_finished.php

Checking database got created ... OK
Checking nominatim.so module installed ... OK
Checking place table ... OK
Checking indexing status ... OK
Search index creation
Checking index idx_word_word_id ... OK
Checking index idx_place_addressline_address_place_id ... OK
Checking index idx_placex_rank_search ... OK
Checking index idx_placex_rank_address ... OK
Checking index idx_placex_pendingsector ... OK
Checking index idx_placex_parent_place_id ... OK
Checking index idx_placex_geometry_reverse_lookuppoint ... OK
Checking index idx_placex_geometry_reverse_lookuppolygon ... OK
Checking index idx_placex_geometry_reverse_placenode ... OK
Checking index idx_location_area_country_place_id ... OK
Checking index idx_osmline_parent_place_id ... OK
Checking index idx_osmline_parent_osm_id ... OK
Checking index idx_place_osm_unique ... OK
Checking index idx_postcode_id ... OK
Checking index idx_postcode_postcode ... OK
Checking index idx_search_name_nameaddress_vector ... OK
Checking index idx_search_name_name_vector ... OK
Checking index idx_search_name_centroid ... OK
Checking search indices are valid ... OK
        

Exit out of the postgres user.

exit

4. Set up web server

Edit the tile server configuration file.

sudo vi /etc/apache2/sites-enabled/tiles_site-le-ssl.conf

Add the following lines between the VirtualHost tags.
        <Directory "/srv/nominatim/build/website">
          Options FollowSymLinks MultiViews
          AddType application/json   .php
          DirectoryIndex search.php
          Require all granted
        </Directory>
        alias /nominatim /srv/nominatim/build/website
        

Save and close the file. Then reload Apache.

sudo systemctl reload apache2

Now visit https://tile.yourdomain.com/nominatim. You will see your Nomiatim instance.

5. Add search functionality to a slippy map

I assume your slippy map is displayed using the Leaflet JavaScript library. To add search functionality to your map, you need to use a Leaflet geocoding plugin. I will show you how to use Leaflet Control Geocoder. It’s actually very simple.

Suppose you used the following HTML code to display your slippy map.

    <html>
    <head>
    <meta charset="UTF-8">
    <title>My first osm</title>
    <link rel="stylesheet" type="text/css" href="html/leaflet.css"/>
    <script src="html/leaflet.js"></script>
    <style>
       html{height:100%;}
       body{height:100%;}
       
       #map{width:100%;height:100%}
    </style>
    </head>

    <body>
      <div id="map"></div>
      <script>
        var map = L.map('map').setView([32,117],7);
        L.tileLayer('http://tile.your-domain.com/osm/{z}/{x}/{y}.png',{maxZoom:18}).addTo(map);
      </script>
    </body>
    </html>
        

Now you need to add the following two lines in the HTML header to use the Leaflet Control Geocoder plugin. First, download Geocoder CSS and JavaScript from cdnjs (https://cdnjs.com/libraries/perliedman-leaflet-control-geocoder/2.4.0) into your website folder. Then include the two files in the head section.

<link rel="stylesheet" href="Control.Geocoder.min.css" />
<script src="Control.Geocoder.min.js"></script>

Then add the following function to the <script>...</script> code so the search functionality will be added to your map.

L.Control.geocoder().addTo(map);

The final HTML code look like this:

    <html>
        <head>
            <meta charset="UTF-8">
            <title>My first osm</title>
            <link rel="stylesheet" type="text/css" href="html/leaflet.css"/>  
            <link rel="stylesheet" href="Control.Geocoder.min.css " />
            <script src="html/leaflet.js"></script>
            <script src="Control.Geocoder.min.js"></script>
            <style>
               html{height:100%;}
               body{height:100%;}
               #map{width:100%;height:100%;}
            </style>
        </head>

        <body>
            <div id="map"></div>
            <script>
               var map = L.map('map').setView([32,117],7);
               L.tileLayer('https://tile.yourdomain.com/osm/{z}/{x}/{y}.png',{maxZoom:18}).addTo(map); 
               L.Control.geocoder.addTo(map);               
             </script>
       </body>
    </html>
        

Save and close the file. Then reload the map in your web browser, you should see a search button on the upper-right corner.

By default, Leaflet Control Geocoder uses the public https://nominatim.openstreetmap.org geocoding service. To make it use your own Nominatim geocoding service, delete the following line:

L.Control.geocoder().addTo(map);

Add the following lines instead. Replace the URL with the URL of your Nominatim geocoding service. Note that you should not leave out the trailing slash.

      var geocoder = L.Control.Geocoder.nominatim({serviceUrl:'https://tile.yourdomain.com/nominatim/'});
      var control = L.Control.geocoder({
        placeholder: 'Search here...',
        geocoder: geocoder,
        position: 'topright'
      }).addTo(map);
        

You can also add the following code for reverse geocoding. When a visitor clicks on a point on the map, the name of that address will appear.

    map.on('click', function(e) {
        var lat = e.latlng.lat;
        var lon = e.latlng.lng;
        var lonmap = lon;       /* The X in leaflet might be larger than 180, so needs reduction */
        while (lonmap > 180) {
            lonmap -= 360;
        }
        while (lonmap < -180) {
            lonmap += 360;
        }
        var nominatim1 = 'https://tile.yourdomain.com/nominatim/reverse?format=json&lat=';

        var xhttp = new XMLHttpRequest();

        xhttp.addEventListener("load", function(event) {
            var myobj = JSON.parse(event.target.response);  
            if (myobj && myobj.address) {
                var address = myobj.display_name;
                L.marker([lat, lon]).addTo(map)
                    .bindPopup(address)
                    .openPopup();
            } else {
                console.log('Address not found.');
            }
        });

        // set up our request
        xhttp.open("GET", nominatim1 + lat + '&lon=' + lonmap);
        xhttp.send();
        console.log(lat.toFixed(6) + ", " + lon.toFixed(6));
    });
        

Save and close the file. Then reload the map in your web browser.

6. Integrate routing machine and search functionality

I assume your slippy map is displayed using the Leaflet JavaScript library, and you have added Nominatim geocoding service to your slippy map.

To integrate routing machine and search functionality, we can use a plugin called Leaflet Routing Machine. First, include the Leaflet routing machine JavaScript and CSS file to your slippy map. Note that they should be placed after the main Leaflet JavaScript and the Leaflet Control Geocoder JavaScript.

Download leaflet-routing-machine.css, leaflet-routing-machine.min.js, and leaflet.routing.icons.png and routing-icon.png into dist folder:

sudo mkdir /var/www/html/dist
cd /var/www/html/dist
sudo wget https://unpkg.com/leaflet-routing-machine@latest/dist/leaflet-routing-machine.css
sudo wget https://unpkg.com/leaflet-routing-machine@latest/dist/leaflet-routing-machine.js
sudo wget https://unpkg.com/leaflet-routing-machine@latest/dist/leaflet.routing.icons.png
sudo wget https://unpkg.com/leaflet-routing-machine@latest/dist/routing-icon.png

        <html>
          <head>
             ....
             ....
             <link rel="stylesheet" href="dist/leaflet-routing-machine.css" />
             <script src="dist/leaflet-routing-machine.js"></script>
         </head>
         <body>
         ....
         ....
         </body>
        </html>
        
        

Next, add the following lines to the <script>...</script> snippet in the HTML body.

    L.Routing.control({
        serviceUrl: 'https://osrm.your-domain.com/route/v1',
        geocoder: L.Control.Geocoder.nominatim({serviceUrl:'https://tile.your-domain.com/nominatim/'}),
        reverseWaypoints: true,
        routeWhileDragging: true
    }).addTo(map);

        
Like this:
        <html>
          <head>
             ....
             ....
             <link rel="stylesheet" href="dist/leaflet-routing-machine.css" />
             <script src="dist/leaflet-routing-machine.js"></script>
         </head>
         <body>
           <div id="map"></div>
             <script>
             ....
             ....
	     L.Routing.control({
                serviceUrl: 'https://osrm.your-domain.com/route/v1',
                geocoder: L.Control.Geocoder.nominatim({serviceUrl:'https://tile.your-domain.com/nominatim/'}),
                reverseWaypoints: true,
                routeWhileDragging: true
             }).addTo(map);
             </script>

         </body>
        </html>
        

Save and close the file. Then reload the map in your web browser, you should see a control panel on the upper-right corner, where you can enter the starting address and destination address.

You can drag the waypoints on the map and OSRM will automatically recalculate the route.

Next Step

I hope this series of tutorials helped you set up OpenStreetMap tile, routing, and geocoding server. You may also want to set up an email server for your domain.