Choose Nominatim when you want a single-container, Postgres-backed OpenStreetMap geocoder that is cheap to stand up and excels at full-address lookups, and choose Pelias when you need type-ahead autocomplete, blended data sources beyond OSM, and horizontal Elasticsearch scaling — both are covered as self-hosted options in the broader comparing geocoding accuracy across providers guide.
Decision Matrix
Self-hosting removes per-request cost and result-storage restrictions but shifts the burden onto your operations team. The choice between these two engines is really a choice about data breadth, autocomplete needs, and how much infrastructure you are willing to run. When either becomes a node in a multi-API routing and fallback chain, these properties decide where it sits.
| Axis | Nominatim | Pelias |
|---|---|---|
| Data sources | OpenStreetMap only | OSM + OpenAddresses + WhosOnFirst + TIGER |
| Storage engine | PostgreSQL + PostGIS | Elasticsearch |
| Architecture | Single service + DB | Microservices (API, PIP, placeholder, importers) |
| Autocomplete | Not a first-class feature | Native /autocomplete endpoint |
| Import time (planet) | Long single import into Postgres | Parallel importers into Elasticsearch |
| Horizontal scaling | Read replicas of Postgres | Shard/replica across ES nodes |
| Hardware footprint | Lower; one box can serve a country | Higher; ES cluster + import pipeline |
| Best fit | Full-address reverse/forward lookups | Autocomplete, multi-source coverage |
Data Sources and Coverage
Nominatim builds exclusively on OpenStreetMap. Where OSM is dense — most of Europe and well-mapped urban areas — its results are excellent, but where OSM lacks house-number coverage, Nominatim inherits those gaps directly. Pelias imports from several sources and reconciles them: OpenStreetMap for streets and POIs, OpenAddresses for authoritative point-level house numbers, WhosOnFirst for administrative hierarchy and place names, and TIGER for US street ranges. That blend means Pelias can return a rooftop-quality point from OpenAddresses in a region where OSM has no house number at all. The trade-off is that reconciling overlapping sources introduces its own deduplication and ranking complexity, which is precisely the kind of problem you also manage when deduplicating addresses by fuzzy canonical key.
Architecture and Autocomplete
Nominatim is comparatively monolithic: a PostgreSQL/PostGIS database plus a query service. That simplicity is its strength — you can run a country extract on a single well-provisioned VM and back it up like any Postgres database. It performs forward and reverse geocoding well but was never designed for keystroke-latency autocomplete; partial-string queries are not its sweet spot.
Pelias is a set of microservices — an API gateway, a point-in-polygon service, a placeholder service for coarse text, and a family of importers — all sitting on Elasticsearch. Elasticsearch’s inverted index is what gives Pelias a genuinely fast /autocomplete endpoint that returns ranked suggestions as a user types. If your product surface includes an address search box, that capability is difficult to replicate on Nominatim and is usually the deciding factor. Whichever engine you front, cache the normalized outputs so repeated lookups never hit the backend twice, following the patterns in Redis and Postgres caching patterns.
Throughput, Scaling, and Maintenance
Nominatim scales reads the way Postgres does: add read replicas, put PgBouncer in front, and cache aggressively. Its planet import is a long, largely single-threaded operation that is memory- and disk-intensive, but once imported, the maintenance model is familiar to any team that runs Postgres. Pelias scales by adding Elasticsearch nodes and tuning shards and replicas, and its importers run in parallel, but you are now operating an Elasticsearch cluster with all the heap-tuning, snapshot, and version-upgrade care that entails. For a small team, Nominatim’s operational surface is smaller; for a team that already runs Elasticsearch, Pelias adds less marginal burden.
Unified Python Client
The client below queries either self-hosted service over HTTP and normalizes both to one schema. Point base_url at your own instance — never at a public demo server for production traffic.
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Optional
import requests
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)
@dataclass
class SelfHostedResult:
"""Common schema for Nominatim and Pelias responses."""
lat: float
lon: float
match_type: str # e.g. house / street / locality
confidence: float # 0-1, normalized across engines
label: str
provider: str
raw_input: str = ""
@property
def is_house_level(self) -> bool:
"""True when the result resolves to a specific address point."""
return self.match_type in {"house", "address", "building"} and self.confidence >= 0.7
def parse_nominatim(address: str, payload: list) -> SelfHostedResult:
"""Parse a Nominatim /search JSON array (jsonv2 format)."""
if not payload:
raise ValueError("Nominatim returned no results")
top = payload[0]
# Nominatim 'importance' is roughly 0-1; use it as a confidence proxy.
return SelfHostedResult(
lat=float(top["lat"]),
lon=float(top["lon"]),
match_type=top.get("addresstype", top.get("type", "unknown")),
confidence=float(top.get("importance", 0.0)),
label=top.get("display_name", ""),
provider="nominatim",
raw_input=address,
)
def parse_pelias(address: str, payload: dict) -> SelfHostedResult:
"""Parse a Pelias /v1/search GeoJSON FeatureCollection."""
features = payload.get("features", [])
if not features:
raise ValueError("Pelias returned no results")
top = features[0]
props = top.get("properties", {})
# Pelias coordinates are [longitude, latitude].
lon, lat = top.get("geometry", {}).get("coordinates", [0.0, 0.0])
return SelfHostedResult(
lat=float(lat),
lon=float(lon),
match_type=props.get("layer", "unknown"),
confidence=float(props.get("confidence", 0.0)),
label=props.get("label", ""),
provider="pelias",
raw_input=address,
)
def geocode(address: str, provider: str, base_url: str) -> SelfHostedResult:
"""Query a self-hosted Nominatim or Pelias instance and normalize it."""
if provider == "nominatim":
url = f"{base_url.rstrip('/')}/search"
params = {"q": address, "format": "jsonv2", "limit": 1, "addressdetails": 1}
resp = requests.get(url, params=params, timeout=10,
headers={"User-Agent": "addr-pipeline/1.0"})
resp.raise_for_status()
return parse_nominatim(address, resp.json())
if provider == "pelias":
url = f"{base_url.rstrip('/')}/v1/search"
params = {"text": address, "size": 1}
resp = requests.get(url, params=params, timeout=10)
resp.raise_for_status()
return parse_pelias(address, resp.json())
raise ValueError(f"Unsupported provider: {provider!r}")
Vectorized pandas batch
Self-hosted engines have no per-request billing, so batch throughput is bounded by your own hardware. Keep concurrency modest against a single Nominatim box; scale it up only once you have added Postgres replicas or Elasticsearch nodes.
import pandas as pd
def geocode_frame(
df: pd.DataFrame,
address_col: str,
provider: str,
base_url: str,
) -> pd.DataFrame:
"""Geocode every row against a self-hosted engine and append columns."""
def _safe(addr: str) -> dict:
try:
return geocode(addr, provider, base_url).__dict__
except (ValueError, KeyError, requests.RequestException) as exc:
logger.warning("geocode failed for %r: %s", addr, exc)
return {"provider": provider, "raw_input": addr, "confidence": 0.0}
expanded = df[address_col].map(_safe).apply(pd.Series)
return df.join(expanded, rsuffix="_geo")
Edge Cases
Nominatim importance is not a match confidence
Nominatim’s importance reflects how prominent a place is in OSM, not how well it matched your query — a famous landmark scores high even for a loose match. Do not treat it as a verification signal on its own. Corroborate house-level results by comparing the returned display_name components against your input, and calibrate any threshold empirically using confidence-scoring techniques.
Pelias source precedence surprises
Because Pelias blends sources, two runs can return points from different providers (OpenAddresses vs OSM) for the same query after a reimport, shifting a coordinate by several metres. Pin the source parameter (for example sources=openaddresses,osm) when you need deterministic behaviour, and record which source produced each result so a downstream diff does not look like a regression.
Integration Note
Self-hosted engines shine as the cost-free primary or as the final fallback tier in a chain, precisely because they carry no per-request fee or result-storage restriction. A common shape is Pelias fronting an autocomplete box while Nominatim handles server-side batch normalization, with a commercial API reserved for the residual set that neither resolves. Wire that escalation through the fallback chains for failed lookups pattern, and consult the parent comparing geocoding accuracy across providers guide to decide the confidence cut-off at which a self-hosted miss should escalate to a paid provider.
Related
- Comparing Geocoding Accuracy Across Providers — how to benchmark self-hosted engines against commercial APIs on your own labelled corpus.
- Implementing Fallback Chains for Failed Lookups — put Nominatim or Pelias first for cost, then escalate misses to a paid provider.
- Redis and Postgres Caching Patterns — cache self-hosted results so repeated lookups never re-hit your backend.
- Validating Geocoding Accuracy and Confidence Scoring — turn Nominatim
importanceand Peliasconfidenceinto comparable, calibrated scores.