The Question Nominatim Can’t Answer
Say you need a list of every public library in Cook County, Illinois that has wheelchair access and free WiFi. You’ve got a fresh Nominatim stack running. You type in “public library Cook County” and you get… a handful of results, maybe the most famous ones. Nominatim is a geocoder. It turns addresses into coordinates. It doesn’t answer “find me all things matching these tags within this area.”
That’s a different problem. And it has a different tool: Overpass API.
Overpass is a read-only query engine built specifically for OpenStreetMap data. Where Nominatim answers “where is this address?”, Overpass answers “give me every OSM object matching this set of conditions.” Public libraries with wheelchair access and WiFi in a county boundary? Five lines of Overpass Query Language. Every ATM within 300 meters of a subway entrance in Brooklyn? Also five lines. This is the tool you reach for when Nominatim shrugs.
Full example: Compose + region init script + sample queries at github.com/KingPin/sumguy-examples/tree/main/self-hosting/overpass-api-self-hosted
What Overpass Is (and Isn’t)
Overpass API is a read-only database and query service that mirrors OpenStreetMap data. It’s maintained separately from the main OSM database, you write queries, it returns matching nodes, ways, and relations. No writes, no geocoding, no tile rendering. Just “find me stuff matching these conditions.”
The query language is called OQL (Overpass Query Language). It looks a bit like a programming language crossed with SQL crossed with something you’d invent at 2 AM. Once it clicks, it’s surprisingly expressive.
The public instance at overpass-api.de (and mirrors like overpass.private.coffee) handles a massive amount of traffic. For one-off queries and experiments, it’s fine. For anything running on a schedule, a scraper, a data pipeline, a nightly job that generates a CSV of new restaurants in your city, you’re going to hit rate limits fast and your requests will start timing out. That’s not a complaint; it’s a free public service. Self-hosting exists for the workloads that go beyond “I’m poking at it in a browser.”
One more important distinction: Overpass is not Nominatim and Nominatim is not Overpass. They answer different questions. In practice, you often use them together, Nominatim to look up an area by name, Overpass to query what’s in that area. We’ll get to that pairing.
OQL Basics: Nodes, Ways, Relations
OpenStreetMap stores everything as one of three object types:
- node: a single point with lat/lon. A bench, an ATM, a tree, a bus stop.
- way: an ordered sequence of nodes forming a line or polygon. A road, a building footprint, a river.
- relation: a collection of nodes, ways, and other relations grouped with semantic meaning. A bus route, a multipolygon park, a turn restriction.
Overpass queries filter these by tags. OSM tags are key-value pairs: amenity=cafe, wheelchair=yes, internet_access=wlan. Everything interesting in OSM is a tag.
The simplest OQL query:
[out:json][timeout:25];node["amenity"="cafe"](40.6,-74.05,40.75,-73.9);out body;Breaking this down:
[out:json]: return JSON (alternatives:xml,csv)[timeout:25]: give up after 25 seconds if the server is strugglingnode["amenity"="cafe"]: find nodes taggedamenity=cafe(40.6,-74.05,40.75,-73.9): bounding box filter: south lat, west lon, north lat, east lonout body;: return the full object data
The bounding box is the simplest spatial filter. But Overpass supports more useful ones:
Area filter, match by a named area (like a city, county, or neighborhood):
[out:json][timeout:30];area["name"="Brooklyn"]["place"="suburb"]->.searchArea;node["amenity"="cafe"](area.searchArea);out body;Around filter, match objects within N meters of another set:
[out:json][timeout:30];node["railway"="subway_entrance"](40.6,-74.05,40.75,-73.9)->.subways;node["amenity"="atm"](around.subways:300);out body;That second query finds every ATM within 300 meters of a subway entrance in the bounding box. That’s the kind of query you’d pay a commercial API for.
Multiple tag filters:
[out:json][timeout:30];area["name"="Cook County"]["admin_level"="6"]->.county;node["amenity"="library"]["wheelchair"="yes"]["internet_access"="wlan"](area.county);out body;There’s your public libraries with wheelchair access and WiFi in Cook County. Welcome to Overpass.
The Public Instance and Its Limits
The public Overpass instances (overpass-api.de and mirrors) are useful. They’re running against a near-live copy of the full planet, updates come in roughly every minute. Overpass Turbo at overpass-turbo.eu is a browser-based IDE that runs queries against the public instance and displays results on a map. It’s the fastest way to prototype a query before wiring it into code.
The rate limits kick in when you start running queries programmatically. The public instances enforce a concurrency limit per IP and will reject or queue requests when they’re under load. Complex spatial queries on large areas eat through the timeout budget fast. You also have no control over the data version, if you need a consistent snapshot for a pipeline, public Overpass is the wrong tool.
None of this is a criticism. It’s a free public service for a planet-sized dataset. The message is just: for any recurring or production workload, run your own.
Self-Hosted Setup
The standard Docker image is wiktorn/overpass-api. It handles the initial OSM data import and runs the Overpass backend. Unlike Nominatim, Overpass doesn’t need PostGIS, it uses its own custom flat-file storage format, which is part of why it’s fast.
Here’s a working Compose stack:
services: overpass: image: wiktorn/overpass-api:latest container_name: overpass ports: - "12345:80" environment: OVERPASS_META: "yes" OVERPASS_MODE: "init" OVERPASS_PLANET_URL: "https://download.geofabrik.de/north-america/us/new-york-latest.osm.pbf" OVERPASS_DIFF_URL: "https://download.geofabrik.de/north-america/us/new-york-updates/" OVERPASS_RULES_LOAD: "10" OVERPASS_SPACE: "4000000000" OVERPASS_MAX_TIMEOUT: "1000" volumes: - overpass-db:/db restart: unless-stopped
volumes: overpass-db:The critical env vars:
OVERPASS_MODE: "init": runs the initial import, then switches to serving mode automaticallyOVERPASS_PLANET_URL: the PBF file to import. Use a regional Geofabrik extract, not the planetOVERPASS_DIFF_URL: Geofabrik’s diff feed for incremental updates (keeps the data fresh)OVERPASS_SPACE: disk space in bytes that Overpass is allowed to use for its database. 4 GB here for a state extract; scale up for larger regionsOVERPASS_META: "yes": store node metadata (timestamps, user info). Required for some advanced queries; skip it if you want a smaller footprint
Bring it up:
docker compose up -ddocker compose logs -f overpassThe import takes a few minutes for a state-level extract, up to a couple of hours for a full continent. Once it’s done, the same container switches to serving mode and starts answering queries on port 12345.
Hardware sizing:
| Extract size | Disk needed | RAM recommendation |
|---|---|---|
| Single city/region (~50 MB PBF) | ~500 MB | 1 GB |
| Single US state (~300 MB PBF) | ~3 to 5 GB | 2 to 4 GB |
| Full continent (~10 GB PBF) | ~100+ GB | 8 to 16 GB |
| Full planet (~80 GB PBF) | ~500+ GB | 32+ GB |
For most self-hosters, a state or small country extract covers it. You cover your actual use case without needing a NAS for the database.
Querying Your Instance
Once the import finishes, hit it with curl:
curl "http://localhost:12345/api/interpreter?data=[out:json][timeout:25];node[\"amenity\"=\"cafe\"](40.6,-74.05,40.75,-73.9);out body;"URL-encoding the brackets is annoying for one-offs. Use a POST request with the query in the body, cleaner and avoids shell quoting hell:
curl -s -X POST "http://localhost:12345/api/interpreter" \ --data-urlencode 'data=[out:json][timeout:25];node["amenity"="cafe"](40.6,-74.05,40.75,-73.9);out body;'The response is a JSON object with an elements array. Each element has type, id, lat, lon, and tags:
{ "version": 0.6, "elements": [ { "type": "node", "id": 123456789, "lat": 40.691, "lon": -73.987, "tags": { "amenity": "cafe", "name": "Some Coffee Place", "opening_hours": "Mo-Fr 07:00-18:00" } } ]}Producing a CSV, useful for spreadsheets, data pipelines, QGIS imports. Overpass has native CSV output:
[out:csv(name, lat, lon, "opening_hours", "wheelchair"; true; ",")][timeout:30];area["name"="Brooklyn"]["place"="suburb"]->.searchArea;node["amenity"="cafe"](area.searchArea);out;The out:csv(...) header takes: column names (OSM tags), header row (true/false), and delimiter. That query outputs something you can pipe straight into a file and open in LibreOffice Calc.
curl -s -X POST "http://localhost:12345/api/interpreter" \ --data-urlencode 'data=[out:csv(name,lat,lon,"opening_hours","wheelchair";true;",")][timeout:30];area["name"="Brooklyn"]["place"="suburb"]->.searchArea;node["amenity"="cafe"](area.searchArea);out;' > brooklyn_cafes.csvPerformance and Tuning
Complex Overpass queries are expensive. Understand why and you’ll write better queries.
Overpass’s storage is area-indexed, it can efficiently find all objects within a bounding box. The problem comes when you chain complex operations: find all subway entrances in this area, then for each one find all ATMs within 300 meters. That’s a (around:...) query. It works, but it scans a lot of data. On a planet database, this can run for minutes.
[timeout:N], always set this. The public servers enforce it; your self-hosted instance will run queries until they finish or the server OOMs if you forget it. 25 to 60 seconds is reasonable for most queries. Raise it for complex spatial joins. The container’s OVERPASS_MAX_TIMEOUT env var caps what clients can request.
[maxsize:N], limits the response size in bytes. Default is 536870912 (512 MB). If a query returns millions of objects, you probably didn’t mean to. Add [maxsize:50000000] (50 MB) to catch runaway queries early.
Prefer bounding box over area where possible. Area lookups require Overpass to do a named-area lookup first, then the spatial filter. A tight bounding box is faster. Use area when you need the named-area semantics (county boundaries, city limits) and can’t express it as a rectangle.
Split large queries. If you’re processing a whole continent, split it into smaller regions and run sequentially or parallel against multiple instances. Overpass is single-threaded per query.
out skel; vs out body;, skel returns only IDs and geometry (no tags). If you only need coordinates and not the tag data, use skel. Much smaller responses.
Pairing with Nominatim
This is where the combo really shines. Nominatim resolves a human-readable area name to an OSM area ID. Overpass uses that area ID to scope a query.
Workflow:
- Nominatim:
GET /search?q=Cook+County+Illinois&format=json→ get the OSM relation ID fromosm_idin the response - Overpass: use
area(ID+3600000000)to turn that relation ID into an Overpass area reference
# Step 1: Get Cook County's OSM relation ID from your Nominatim instanceRELATION_ID=$(curl -s "http://nominatim.lan/search?q=Cook+County+Illinois&format=json" \ | python3 -c "import sys,json; print([x for x in json.load(sys.stdin) if x['osm_type']=='relation'][0]['osm_id'])")
# Step 2: Query Overpass using that area ID (relation IDs get +3600000000)AREA_ID=$((RELATION_ID + 3600000000))
curl -s -X POST "http://localhost:12345/api/interpreter" \ --data-urlencode "data=[out:json][timeout:60];area($AREA_ID)->.county;node[\"amenity\"=\"library\"][\"wheelchair\"=\"yes\"](area.county);out body;" | python3 -c "import sys, jsondata = json.load(sys.stdin)for el in data['elements']: name = el['tags'].get('name', 'Unknown') print(f\"{name},{el['lat']},{el['lon']}\")"That’s a complete pipeline: area resolution from Nominatim, spatial tag query in Overpass, CSV output. Two self-hosted services, zero API keys, no rate-limit anxiety.
Overpass Turbo Against Your Own Instance
Overpass Turbo (overpass-turbo.eu) is the public IDE for building and testing queries. It defaults to the public Overpass instances, but you can point it at your self-hosted one.
In Overpass Turbo, click Settings → Server. Set the custom server to http://your-host:12345/api/interpreter. Now every query you run in the IDE hits your instance. Full map visualization, query builder, export, all against your data. This is the best way to iterate on complex queries before wiring them into a script.
One gotcha: if your Overpass instance is on a private network and Turbo is running in your browser, the browser needs to be able to reach that host. If it’s on a Tailscale/Wireguard network that your laptop is on, you’re fine. If it’s a bare server IP behind NAT without any tunnel, you’ll need to either put Overpass behind a reverse proxy with a proper hostname, or run a local copy of the Overpass Turbo frontend yourself. The wiktorn/overpass-api image doesn’t bundle one: the container’s / just serves the stock nginx welcome page, not a query tool. Clone overpass-turbo and serve it as static files behind your reverse proxy if you want the UI on the LAN.
When You Actually Need This
Overpass clicks for a specific class of problems:
- POI extraction: “give me every restaurant in this city as a CSV for a mapping project”
- Change detection: compare two Overpass snapshots to see what changed in an area
- Proximity analysis: what’s within N meters of a set of features
- Tag auditing: find all OSM objects in a region missing a required tag (useful for OSM editors)
- Custom geocoding context: after Nominatim gives you a location, find what’s around it
It’s not the tool for: address lookup (Nominatim), tile serving (OpenMapTiles/Tegola), routing (OSRM/Valhalla), or anything that needs real-time OSM data (Overpass lags by a few minutes even on the public instances, and your self-hosted one lags by however often you run the diff update).
Self-hosting the full planet is mostly unnecessary. A regional extract covers 99% of real use cases and you’re back up and running in under an hour. The public instance is fine for prototyping and one-offs. The moment you have a repeating job, a data pipeline, or any kind of SLA, that’s when your own instance earns its keep, and it’ll do it on modest hardware.
Your 2 AM self will appreciate not reading “rate limit exceeded” at step 3 of a 10,000-item import.
Related posts
- Nominatim: Self-Hosted Geocoding, address lookup, different problem
- The Full Self-Hosted Maps Stack: Nominatim + PostGIS + Tiles, go deeper on the OSM toolchain
- PostGIS for Self-Hosted Mapping: if you want to store and query your own spatial data