Importing Nominatim from an OSM Extract Jump to heading
Stand up a private geocoder from a regional .osm.pbf so a batch of a hundred thousand addresses becomes a twenty-minute job instead of a day of throttled requests against a shared service.
Prerequisites Jump to heading
Conceptual minimum Jump to heading
A Nominatim import is not a copy of the extract; it is a purpose-built search database derived from it. The import reads OSM elements, decides which are places worth indexing, computes an address hierarchy by working out which administrative and named areas contain each place, and writes a token index used to match query text.
Three parts of that pipeline dominate the runtime and the failure modes.
Node location storage. Building way and relation geometry requires the coordinates of every referenced node, and there are far more nodes than places. The import can either keep them in the database — slow and enormous — or in a flatnode file, a fixed-layout file indexed directly by node id. The flatnode file is essentially mandatory for anything country-sized and above, and it must be on fast local disk because access to it is random.
Address computation. Each place is assigned its containing hierarchy: the street, the suburb, the city, the state, the country. This is the stage that makes structured queries work, and it is the most compute-heavy part of the import.
Interpolation. Address ranges mapped as interpolation ways are expanded into individual addressable points. Skipping this pass makes the import faster and leaves entire streets ungeocodable in regions where interpolation is the dominant address mapping style.
Runnable solution Jump to heading
#!/usr/bin/env bash
# Import a private Nominatim instance from a regional extract.
set -euo pipefail
EXTRACT="/data/poland-latest.osm.pbf"
PROJECT_DIR="/srv/nominatim"
FLATNODE="/fast-local/nominatim.flatnode" # MUST be local, random-access disk
THREADS="$(nproc)"
mkdir -p "$PROJECT_DIR" "$(dirname "$FLATNODE")"
cat > "$PROJECT_DIR/.env" <<'ENV'
NOMINATIM_DATABASE_DSN=pgsql:dbname=nominatim
NOMINATIM_FLATNODE_FILE=/fast-local/nominatim.flatnode
# Keep interpolation: in many countries it is the dominant address style.
NOMINATIM_USE_US_TIGER_DATA=false
# The replication source must match the region of the extract, exactly.
NOMINATIM_REPLICATION_URL=https://download.geofabrik.de/europe/poland-updates/
ENV
cd "$PROJECT_DIR"
nominatim import --osm-file "$EXTRACT" --threads "$THREADS" 2>&1 | tee import.log
# Index freshness state so replication can start from the extract's timestamp.
nominatim replication --init
echo "import finished; run the verification script before trusting results"
Verification belongs in code, not in a manual spot check:
from __future__ import annotations
import logging
from dataclasses import dataclass
import requests
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger("osm.nominatim.verify")
LOCAL = "http://localhost:8080"
TOLERANCE_DEG = 0.01 # roughly a kilometre; an import check, not a precision one
@dataclass(frozen=True)
class Known:
street: str
city: str
country: str
lat: float
lon: float
def lookup(sample: Known) -> dict | None:
params = {
"street": sample.street, "city": sample.city,
"countrycodes": sample.country, "format": "jsonv2",
"addressdetails": 1, "limit": 1,
}
response = requests.get(f"{LOCAL}/search", params=params, timeout=30)
response.raise_for_status()
results = response.json()
return results[0] if results else None
def verify(samples: list[Known]) -> bool:
ok = True
for sample in samples:
hit = lookup(sample)
if hit is None:
logger.error("no match for %s, %s — import may be incomplete",
sample.street, sample.city)
ok = False
continue
dlat = abs(float(hit["lat"]) - sample.lat)
dlon = abs(float(hit["lon"]) - sample.lon)
rank = int(hit.get("place_rank", -1))
if dlat > TOLERANCE_DEG or dlon > TOLERANCE_DEG:
logger.error("%s: matched %.4f,%.4f, expected %.4f,%.4f",
sample.street, float(hit["lat"]), float(hit["lon"]),
sample.lat, sample.lon)
ok = False
elif rank < 26:
# A coarse rank means the street did not match; the hierarchy
# (address computation) stage probably did not complete.
logger.warning("%s: matched at place_rank %d — street-level or coarser",
sample.street, rank)
else:
logger.info("%s: ok at place_rank %d", sample.street, rank)
return ok
if __name__ == "__main__":
known = [
Known("Rynek Główny", "Kraków", "pl", 50.0617, 19.9373),
Known("Krupówki", "Zakopane", "pl", 49.2969, 19.9490),
]
logger.info("import verification %s", "PASSED" if verify(known) else "FAILED")
Step-by-step walkthrough Jump to heading
- Put the flatnode file on local disk. It is accessed randomly by node id throughout the load stage. On network storage the import can take an order of magnitude longer, and no amount of extra CPU compensates.
- Set the replication URL to match the extract’s region. A country database updated from a continent’s diff directory receives changes for territory it does not contain. Nothing warns you.
- Give the import all the cores. The load and address stages parallelise reasonably well; the index stage less so. Expect the machine to be busy for hours on a country extract.
- Initialise replication immediately after the import. The state is derived from the extract’s own timestamp, so doing it later means guessing, and guessing means either a gap or a redundant replay.
- Verify against coordinates you already know. The verification script checks three things at once: that the address matched at all, that it matched near the right place, and that it matched at a fine enough rank to be a real street match rather than a fallback.
- Treat a coarse place rank as a failure signal. A result that resolves to a city when a street was supplied usually means the address computation stage did not complete, which is a far more common outcome than a completely failed import.
- Keep the import log. When results look wrong three weeks later, the stage timings in the log are the fastest way to see whether a stage was skipped or cut short.
Verification Jump to heading
- Known addresses resolve near their known coordinates. Within a kilometre is a generous tolerance that still catches a wholesale import failure.
- Street-level queries return a fine place rank. A coarse rank across the board means address computation did not finish.
- Interpolated addresses resolve. Pick a house number known to exist only as part of an interpolation range; if it fails, that pass was skipped.
- Reverse geocoding returns a sensible hierarchy. A reverse lookup at a known city centre should return the city, not just the country.
- Replication state is initialised. The stored sequence must correspond to the extract’s timestamp, not to the present moment.
Common errors and fixes Jump to heading
| Symptom | Root cause | One-line fix |
|---|---|---|
| Import runs for days | Flatnode file on network storage | Move it to local disk and restart the import |
| Every result is city-level | Address computation did not complete | Check the log for the stage; rerun the indexing step |
| House numbers never match | Interpolation pass skipped | Re-enable interpolation and re-run the import |
| Out of disk near the end | Sized on the extract, not the database | Provision eight to fifteen times the extract size |
| Results drift from the public instance | Replication points at the wrong region | Match the replication URL to the extract exactly |
| Replication replays months of diffs | State initialised after the import, from “now” | Initialise replication immediately post-import |
| Queries slow after months of updates | Index bloat from continuous updates | Schedule a periodic re-import |
Specification reference Jump to heading
The Nominatim import reads an OSM file into a PostgreSQL/PostGIS database in distinct stages — loading, ranking, address computation and indexing — and uses an optional flatnode file to store node coordinates outside the database, which is recommended for imports of a country or larger. Replication state is initialised from the imported data’s timestamp. See the Nominatim installation and import documentation for stage-by-stage requirements and the current sizing guidance.
Frequently Asked Questions Jump to heading
How much disk does a Nominatim import really need?
Between eight and fifteen times the source extract, depending on how densely the region is addressed. The place and address tables dominate, the search index adds substantially, and the flatnode file is sized by the highest node identifier rather than by the region, so it is surprisingly large even for a small country. Provision the upper end: running out during the address stage means restarting a multi-hour job from the beginning.
Do I need a flatnode file for a single country?
In practice yes. Without it, node coordinates are stored in the database and accessed through it, which turns the load stage into a database-bound crawl and inflates the database size considerably. The flatnode file is indexed directly by node identifier and is much faster, provided it sits on local random-access storage. Putting it on a network volume gives you the worst of both approaches.
Why do all my results come back at city level?
Because the address computation stage did not complete. That stage assigns every place its containing hierarchy, and without it the geocoder can still match a settlement name but has nothing finer to offer for a street or a house number. Check the import log for the stage’s timings, and re-run the indexing step rather than assuming the whole import failed — the earlier stages are usually intact.
Should I attach replication updates or re-import periodically?
It depends on whether freshness or reproducibility matters more. Replication keeps the database within hours of the live map but runs continuously and slowly degrades query performance. A periodic re-import gives predictable performance and a clean database, at the cost of being a batch job. If the geocoded results will ever be defended in an audit, consider pinning one import and never updating it, because that is the only way the same input keeps producing the same output.
Related Jump to heading
- Nominatim Geocoding Pipelines — the parent topic and the ranking model the import produces.
- Batch Geocoding with Nominatim Without Getting Blocked — the workload that motivates this import.
- Running a Local Overpass Instance for Bulk Queries — the sibling self-hosting decision for query workloads.
- Replication Sequence Numbers & State — the state model the update loop keeps.
- Pinning a Reproducible OSM Snapshot by Sequence Number — how to make the pinned-import option genuinely reproducible.
Up one level: Nominatim Geocoding Pipelines.