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).
- nominatim freeze: make database read-only
- nominatim replication: Update the database using an online replication service.
- nominatim add-data: Add additional data from a file or an online source.
- nominatim index: Reindex all new and modified data.
- nominatim admin: Analyse and maintain the database.
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
wget https://www.nominatim.org/data/us_postcode_data.sql.gz
wget https://www.nominatim.org/data/gb_postcode_data.sql.gz
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
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
sudo a2enmod mpm_event
sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.2-fpm
- mpm_event: Uses asynchronous, non-blocking processes, making it very efficient for modern web serving.
- PHP-FPM (FastCGI Process Manager): A separate process manager for PHP that communicates with Apache (via mod_proxy_fcgi), allowing Apache to stay with mpm_event and handle PHP requests efficiently without the overhead of mod_php.
- mod_php (Avoid): Ties PHP directly into Apache's processes, forcing the use of mpm_prefork, which is less scalable and uses more memory.
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"
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.