Geocoding server

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. 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 Geocoding with web interface guide if you want to set up Nominatim geocoding service and web interface in one single step.

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 liblua5.4-dev libexpat1-dev zlib1g-dev libbz2-dev libpq-dev libproj-dev php php-pgsql php-intl php-cgi phpunit php-codesniffer nlohmann-json3-dev python3-setuptools python3-dev python3-pip python3-psycopg2 python3-tidylib python3-behave python3-pytest php-cli python-babel-localedata python3-babel python3-datrie python3-jinja2 python3-markupsafe python3-psutil pylint git pandoc python3-argparse-manpage clang-tidy postgresql-server-dev-15

To avoid tokenizer error in the data importing process, install python3-icu as well:

sudo apt install python3-icu

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-4.3.1.tar.bz2

If your server has only IPv6, you can download it locally and then upload it to the server.

Extract the tarball.

tar xvf Nominatim-4.3.1.tar.bz2

Create the build directory.

mkdir build

Change to this directory and configure the build environment.

cd build
cmake /srv/nominatim/Nominatim-4.3.1

    -- The C compiler identification is GNU 12.2.0
    -- The CXX compiler identification is GNU 12.2.0
    -- Detecting C compiler ABI info
    -- Detecting C compiler ABI info - done
    -- Check for working C compiler: /usr/bin/cc - skipped
    -- Detecting C compile features
    -- Detecting C compile features - done
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Check for working CXX compiler: /usr/bin/c++ - skipped
    -- Detecting CXX compile features
    -- Detecting CXX compile features - done
    -- Found Git: /usr/bin/git (found version "2.39.5")
    -- Building osm2pgsql 1.9.2
    -- Found ZLIB: /usr/lib/x86_64-linux-gnu/libz.so (found version "1.2.13")
    ……
        

Compile the source code.

make

    [ 55%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/taginfo.cpp.o
    [ 57%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/tagtransform-c.cpp.o
    [ 59%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/tagtransform.cpp.o
    [ 61%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/tile.cpp.o
    [ 62%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/thread-pool.cpp.o
    [ 64%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/util.cpp.o
    [ 66%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/wildcmp.cpp.o
    [ 67%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/wkb.cpp.o
    [ 69%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/flex-index.cpp.o
    [ 71%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/flex-table.cpp.o
    [ 72%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/flex-table-column.cpp.o
    [ 74%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/flex-lua-expire-output.cpp.o
    [ 76%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/flex-lua-geom.cpp.o
    [ 77%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/flex-lua-index.cpp.o
    [ 79%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/flex-lua-table.cpp.o
    [ 81%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/flex-write.cpp.o
    [ 83%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/geom-transform.cpp.o
    [ 84%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/lua-setup.cpp.o
    [ 86%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/lua-utils.cpp.o
    [ 88%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/output-flex.cpp.o
    [ 89%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/tagtransform-lua.cpp.o
    [ 91%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/lua-init.cpp.o
    [ 93%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/version.cpp.o
    [ 94%] Building CXX object osm2pgsql/src/CMakeFiles/osm2pgsql_lib.dir/reprojection-generic-proj6.cpp.o
    [ 96%] Linking CXX static library libosm2pgsql.a
    [ 96%] Built target osm2pgsql_lib
    [ 98%] Building CXX object osm2pgsql/CMakeFiles/osm2pgsql.dir/src/osm2pgsql.cpp.o
    [100%] Linking CXX executable osm2pgsql
    [100%] Built target osm2pgsql
    xtao@tile:/srv/nominatim/build$
        

Install Nominatim.

sudo make install

    xtao@tile:/srv/nominatim/build$ sudo make install
    [sudo] password for xtao:
    [ 96%] Built target osm2pgsql_lib
    [100%] Built target osm2pgsql
    Install the project...
    -- Install configuration: ""
    -- Installing: /usr/local/bin/nominatim
    -- Installing: /usr/local/lib/nominatim/lib-python/nominatim
    -- Installing: /usr/local/lib/nominatim/lib-python/nominatim/version.py
    -- Installing: /usr/local/lib/nominatim/lib-python/nominatim/api
    -- Installing: /usr/local/lib/nominatim/lib-python/nominatim/api/search
    -- Installing: /usr/local/lib/nominatim/lib-python/nominatim/api/search/db_search_builder.py
    -- Installing: /usr/local/lib/nominatim/lib-python/nominatim/api/search/legacy_tokenizer.py
    -- Installing: /usr/local/lib/nominatim/lib-python/nominatim/api/search/query.py
    -- Installing: /usr/local/lib/nominatim/lib-python/nominatim/api/search/db_search_fields.py
    -- Installing: /usr/local/lib/nominatim/lib-python/nominatim/api/search/query_analyzer_factory.py
    -- Installing: /usr/local/lib/nominatim/lib-python/nominatim/api/search/icu_tokenizer.py
    -- Installing: /usr/local/lib/nominatim/lib-python/nominatim/api/search/geocoder.py
    -- Installing: /usr/local/lib/nominatim/lib-python/nominatim/api/search/token_assignment.py
    ......
        

2. Import OSM database

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

cd /srv/nominatim/build/
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

Again, the download URL requires IPv4. If needed, you can download them locally and then upload to the server.

Install required Python modules.

sudo apt install python3-dotenv python3-sqlalchemy python3-asyncpg

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/

You can start the screen command now, as the following processes may take long time.
Switch to the postgres user.

sudo -u postgres -i

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

cd /srv/nominatim/build/
/usr/local/bin/nominatim import -j 6 --osm-file /path/to/the/map.osm.pbf 2>&1 | tee setup.log

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

Sample output:

        Using project directory: /srv/nominatim/build
        Creating database
        Setting up country tables
        Importing OSM data file
        osm2pgsql version 1.9.2
        Database version: 15.14 (Debian 15.14-1.pgdg12+1)
        PostGIS version: 3.6
        Storing properties to table public.osm2pgsql_properties.
        Reading input files done in 133s (2m 13s)
          Processed 19208552 nodes in 64s (1m 4s) – 300k/s
          Processed 2269941 ways in 64s (1m 4s) – 35k/s
          Processed 38665 relations in 5s – 8k/s
        No marked ways (Skipping stage 2).
        Clustering table ‘place’ by geometry…
        No indexes to create on table ‘place’.
        Creating id index on table ‘place’…
        Analyzing table ‘place’…
        Done postprocessing on table ‘planet_osm_nodes’ in 0s
        Building index on table ‘planet_osm_ways’
        Done postprocessing on table ‘planet_osm_ways’ in 29s
        Building index on table ‘planet_osm_rels’
        Done postprocessing on table ‘planet_osm_rels’ in 0s
        All postprocessing on table ‘place’ done in 29s.
        Storing properties to table public.osm2pgsql_properties.
        osm2pgsql took 194s (3m 14s) overall.
        Importing Wikipedia importance data
        Importing secondary importance raster data
        Secondary importance file not imported. Falling back to default ranking.
        Create functions (1st pass)
        Create tables
        Create functions (2nd pass)
        Crate table triggers
        Create partition tables
        Create functions (3rd pass)
        Initialise tables
        Load data into placex table…………………………..
        

The loading data process may take up to 10 minutes, so you can press Ctrl + A and then press d to detach from the screen. It may show:

[detached from 31226.pts-0.tile]

You can restore the previous screen process using the following commands:

screen -r number

For my example, it’s 31226, so the command to restore the screen is:

screen -r 31226

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

        Done 19894 in 80 @246.582 per second – rank 26 ETA (seconds): 874.75
        ……
        Done 5720 in 11 @480.525 per second – rank 30 ETA (seconds): 1017.74
        ……
        Done 45920 in 78 @ 594.411 per second -rank 30 ETA (seconds): 754.10
        ……
        Done 5174 in 736 @ 7.029 per second – rank 0 ETA (seconds): 1973.56
        …… 
        Done 19488/19488 in 92 @ 210.816 per second – FINISHED rank 0
        Starting interpolation lines (location_property_osmline) (using batch size 20)
        ……
        Starting indexing postcodes using 6 threads
        Starting postcodes (location_postcode) (using batch size 20)
        ……
        Done 706/706 in 0 @ 962.839 per second - FINISHED postcodes (location_postcode)
        Post-process tables
        Create search index for default country names.
        Recompute word counts
        Setup website at /srv/nominatim/build/website
        

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

nominatim index
nominatim admin --check-database

    postgres@tile:/srv/nominatim/build$ nominatim admin --check-database
    2025-12-13 23:10:16: Using project directory: /srv/nominatim/build
    2025-12-13 23:10:16: Checking database
    Checking database connection ... OK
    Checking for placex table ... OK
    Checking for placex content ... OK
    Checking that tokenizer works ... OK
    Checking for wikipedia/wikidata data ... OK
    Checking indexing status ... OK
    Checking that database indexes are complete ... OK
    Checking that all database indexes are valid ... OK
    Checking TIGER external data table. ... not applicable
    postgres@tile:/srv/nominatim/build$ 
        

You might find the following nominatim commands useful. Details can be obtained from and manual (man nominatim).

Exit out of the postgres user.

exit

3. Set up web server

Edit the tile server configuration file.

sudo vi /etc/apache2/sites-enabled/tileserver_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

Grant permission to the www-data user.

sudo setfacl -R -m u:www-data:rx /srv/nominatim/
sudo chown www-data:www-data /var/www/html/ -R

Dropping data required for dynamic updates

About half of the data in Nominatim's database is not really used for serving the API. It is only there to allow the data to be updated from the latest changes from OSM. For many uses these dynamic updates are not really required. You can drop the dynamic part later using the following command:

nominatim freeze

Note that you still need to provide for sufficient disk space for the initial import. So this option is particularly interesting if you plan to transfer the database or reuse the space later.

How to 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 OSM 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([38.9,-77.3],8);
        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 OSM Geocoder plugin. First, download OSMGeocoder CSS and JavaScript from GitHub (https://github.com/k4r573n/leaflet-control-osm-geocoder) into your website folder. Then include the two files in the head section. You will also need to download the geocoder.png in the images folder.

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

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

var osmGeocoder = new L.Control.OSMGeocoder();
map.addControl(osmGeocoder);

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.OSMGeocoder.css " />
            <script src="html/leaflet.js"></script>
            <script src="Control.OSMGeocoder.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([38.9,-77.3],8);
               L.tileLayer('https://tile.yourdomain.com/osm/{z}/{x}/{y}.png',{maxZoom:18}).addTo(map); 
               var osmGeocoder = new L.Control.OSMGeocoder();
               map.addControl(osmGeocoder);
             </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 OSM Geocoder uses the public https://nominatim.openstreetmap.org geocoding service. To make it use your own Nominatim geocoding service, modify line 178 in Control.OSMGeocoder.js:

var url = protocol + "//nominatim.openstreetmap.org/search" + L.Util.getParamString(params),

Replace the URL with the URL of your Nominatim geocoding service.

var url = protocol + "//tile.yourdomain.com/nominatim/search" + L.Util.getParamString(params),

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.

How to update the Nominatim database

Start screen. Switch to the postgres user.

sudo -u postgres -i

Delete the nominatim database.

dropdb nominatim;

Download Wikipedia importance dump file.

cd /srv/nominatim/build/
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

Download map:

wget http://download.geofabrik.de/north-america/us/maryland-latest.osm.pbf

Run the following command to import OSM extracts to PostgreSQL.

/usr/local/bin/nominatim import -j 6 --osm-file /path/to/the/map.osm.pbf 2>&1 | tee setup.log

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

nominatim index
nominatim admin --check-database

You can also fully vacuum the database to regain space:

vacuumdb -fz nominatim

Exit out of the postgres user.

exit

Enable HTTP2

During the installation process, we installed mod_php, which is incompatible with HTTP2. So, you must disable such module before making HTTP2 working.

sudo a2dismod php8.2
sudo a2dismod mpm_prefork

Purge the old PHP module package:

sudo apt-get purge libapache2-mod-php8.2 # Or simply 'sudo apt-get purge libapache2-mod-php*'

Install PHP-FPM:

sudo apt update
sudo apt install php8.2-fpm

Enable the correct Multi-Processing Module (MPM, Event is recommended for HTTP/2):

sudo a2enmod mpm_event
sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.2-fpm

Why this works:

Offline and online geocoding

Nominatim 4.3 comes with both online and offline geocoding. You get offline geocoding result on Nominatim server directly:

sudo -u postgres -i
nominatim search --query "LeFrak"

It returns:
2026-01-28 20:33:40: Using project directory: /var/lib/postgresql
[
    {
        "place_id": 504952,
        "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright",
        "osm_type": "way",
        "osm_id": 314901003,
        "lat": "38.98366755",
        "lon": "-76.94365744815555",
        "category": "building",
        "type": "university",
        "place_rank": 30,
        "importance": 9.99999999995449e-06,
        "addresstype": "building",
        "name": "LeFrak Hall",
        "display_name": "LeFrak Hall, 7251, Preinkert Drive, Lord Calvert Manor, Calvert Hills, College Park,
        Prince George's County, Maryland, 20742, United States",
        "boundingbox": [
            "38.9835104",
            "38.9838799",
            "-76.9442060",
            "-76.9431145"
        ]
    }
]
        
You can use curl to query online:

curl "https://tile.umd.me.uk/nominatim/search?q=LeFrak"

It returns almost the same result except the name field, as online query is handled by PHP rather than Python in Nominatim 4.3.

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.