Skip to main content
Back to blog

Self-hosting Nominatim for local geocoding

·6 min readSelf-Hosting

Landbound needs geocoding for search. Every hosted API charges per request, and at the scale I was testing, the costs added up fast.

Google's Geocoding API runs $5 per 1,000 requests after a 10,000 monthly free tier. Mapbox gives you 100,000 free temporary geocoding requests per month, which sounds generous until you realize that includes every keystroke in an autocomplete field. The public Nominatim instance at nominatim.openstreetmap.org is free but rate-limited to one request per second. That is fine for occasional lookups. It is not fine for a search feature that users hit repeatedly.

So I set up my own Nominatim instance. It runs on my homelab, costs nothing per request, and handles both forward and reverse geocoding using OpenStreetMap data.

What geocoding actually does

Forward geocoding turns an address or place name into coordinates. You send "Zugspitze, Germany" and get back 47.4211, 10.9853. Reverse geocoding does the opposite: you send coordinates and get a human-readable address.

Both are essential for any mapping application. Landbound needs forward geocoding so users can search for locations, and reverse geocoding so I can display place names when users drop pins on the map.

Running Nominatim with Docker

The mediagis/nominatim Docker image is the easiest way to get a self-hosted instance running. It bundles Nominatim 4.2, PostgreSQL, and the import tooling into a single container.

Here is the docker-compose.yml I use:

version: "3"
services:
  nominatim:
    container_name: nominatim
    image: mediagis/nominatim:4.2
    restart: always
    ports:
      - "8080:8080"
    environment:
      PBF_URL: https://download.geofabrik.de/europe/germany-latest.osm.pbf
      REPLICATION_URL: https://download.geofabrik.de/europe/germany-updates/
      NOMINATIM_PASSWORD: change_this_password
    volumes:
      - nominatim-data:/var/lib/postgresql/14/main
    shm_size: 1gb
volumes:
  nominatim-data:
    driver: local

The PBF_URL tells the container which OpenStreetMap extract to download and import on first startup. I pointed it at the Germany extract from Geofabrik, which is about 4GB as a PBF file. The REPLICATION_URL enables incremental updates so the data stays current without a full reimport.

Start it with docker compose up -d and wait. The Germany import takes a few hours depending on your hardware.

Importing data

Nominatim uses osm2pgsql under the hood to load OpenStreetMap data into PostgreSQL. The Docker image handles this automatically on first boot. You do not need to run osm2pgsql manually unless you are doing a custom installation.

Geofabrik provides free extracts for most countries and regions. The Germany extract is one of the larger single-country files. If you only need geocoding for a specific state or city, grab a smaller extract. Bayern alone is roughly 800MB. Berlin is under 100MB.

The import creates a PostgreSQL database with spatial indexes optimized for geocoding queries. Once it finishes, the API is available at port 8080.

Hardware reality

RAM is the bottleneck. The Nominatim documentation is blunt about this: do not report out-of-memory problems if you have less than 64GB RAM for a planet import. For a full planet, they recommend 128GB or more.

For a single country like Germany, 16GB of RAM works but is tight during the import phase. I run the import on a machine with 32GB and it completes without issues. After the import finishes, runtime memory usage is much lower.

Storage matters too. The Germany extract is 4GB as a PBF file, but the resulting PostgreSQL database is significantly larger. Expect 50-80GB of disk usage for Germany. An SSD is strongly recommended. Running Nominatim on spinning disks turns every query into a patience exercise.

For reference, a full planet import needs at least 1TB of disk space and takes 2-3 days on a fast machine with NVMe drives.

Using the API

Once the container is running and the import is complete, you get a standard Nominatim API. Forward geocoding:

curl "http://localhost:8080/search?q=Marienplatz+Munich&format=json&limit=3"

This returns JSON with coordinates, bounding boxes, and address details:

[
  {
    "place_id": 12345,
    "lat": "48.1371079",
    "lon": "11.5753822",
    "display_name": "Marienplatz, Altstadt-Lehel, Munich, Bavaria, Germany",
    "type": "pedestrian"
  }
]

Reverse geocoding:

curl "http://localhost:8080/reverse?lat=52.52&lon=13.405&format=json"

This returns the address for a given coordinate pair. Both endpoints support format=jsonv2 for a more detailed response format.

No API keys. No rate limits. No per-request costs. The only limit is your server's hardware.

What Nominatim does not do well

Structured address parsing is weaker than Google's. Google handles messy, incomplete, and ambiguous addresses remarkably well. Nominatim is more literal. If the address is slightly wrong or uses unusual formatting, you may not get a result.

There is no autocomplete endpoint out of the box. The public Nominatim API does not support it, and the self-hosted version does not either. If you need typeahead search suggestions, you have to build that layer yourself or use a different tool like Photon, which is a separate geocoder built on top of Nominatim data.

Data freshness depends on how often you run updates. The REPLICATION_URL setting enables incremental updates, but you still need to trigger them. New streets, buildings, and addresses in OpenStreetMap take time to appear in your instance. Google reflects real-world changes faster because they have their own data collection pipeline on top of user contributions.

OpenStreetMap data quality varies by region. Germany has excellent coverage because of an active mapping community. Rural areas in less-mapped countries may have gaps. This is worth testing for your specific use case before committing to self-hosted geocoding.

Is it worth the effort

For Landbound, absolutely. I make thousands of geocoding requests during development and testing alone. At Google's rates, that would cost real money. With a self-hosted Nominatim instance, I can hit the API as hard as I want without thinking about it.

The setup took an afternoon. Most of that time was waiting for the import to finish. The ongoing maintenance is close to zero. I have it running behind Caddy on the same network I wrote about in my home network setup.

If you only need geocoding occasionally, the public Nominatim API or a hosted service with a free tier is the simpler choice. But if geocoding is a core part of your application and you want to control the cost and the infrastructure, running your own instance is straightforward. The OpenStreetMap data model is deep enough for most geocoding needs, and there is a lot more you can do with it beyond address lookups.

Sources

Enjoying the blog? Subscribe via RSS to get new posts in your reader.

Subscribe via RSS