Skip to content
Go back

Overpass API: SQL for OpenStreetMap

By SumGuy 12 min read
Overpass API: SQL for OpenStreetMap
Contents

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:

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:

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:

docker-compose.yml
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:

Bring it up:

Terminal window
docker compose up -d
docker compose logs -f overpass

The 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 sizeDisk neededRAM recommendation
Single city/region (~50 MB PBF)~500 MB1 GB
Single US state (~300 MB PBF)~3 to 5 GB2 to 4 GB
Full continent (~10 GB PBF)~100+ GB8 to 16 GB
Full planet (~80 GB PBF)~500+ GB32+ 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:

Terminal window
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:

Terminal window
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.

Terminal window
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.csv

Performance 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:

  1. Nominatim: GET /search?q=Cook+County+Illinois&format=json → get the OSM relation ID from osm_id in the response
  2. Overpass: use area(ID+3600000000) to turn that relation ID into an Overpass area reference
Terminal window
# Step 1: Get Cook County's OSM relation ID from your Nominatim instance
RELATION_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, json
data = 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:

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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
I Stopped Paying Google Maps API
Next Post
Full Self-Hosted Maps Stack

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts