Overpass QL Intro

Pavel Saman
10 min readApr 20, 2022

OpenStreetMap is probably something I don’t need to introduce much. It’s a map service everyone can contribute to. However, Overpass QL is probably not so known so let’s have a look at it because it’s a great way to unlock the potential of OSM.

We use maps to find stuff. But is it really that easy with, say, Google Maps? How about if I want to find all ATMs in my city that are maximum 100 meters from train stations? How do you do that with Google Maps?

Anytime there’s a need for some slightly advanced search, conventional maps won’t be enough. You can easily perform a full text search, perhaps you can combine it with some boolean operators in Google, but that’s probably about it.

This is a reason why Overpass exists. They have built a way to query OSM. There’s an API for that, or a web-based service called Overpass Turbo. Whichever you want to use, you will have to learn Overpass QL, which is a query language for querying OSM.

Basics of OSM

Before jumping into Overpass QL, I need to explain at least a little bit of how OSM works.

In OSM, there are objects on the map. These objects represent real things like houses, ATMs, gardens, or roads. There can be 3 types of objects:

  • nodes
  • ways
  • relations

Nodes are single points on a map. For example an ATM would likely be represented as a single point on a map.

Ways are ordered lists of nodes. They are used to represent things like houses, rails, power lines, roads. Ways are things that can’t be represented with only a single point.

Finally there are relations. They consist of nodes and ways. They represent things like country borders, bus lines, institution buildings grouped together.

Then you also need to know about tags. Tags are really just properties of the OSM objects. So, a node can have multiple tags, or a way can have multiple tags. Tags tell you more about the thing on the map. Tags are in a key-value format. An example could be amenity=atm. This tag will likely be associated with a node that represents an ATM. You can explore tags on this Wiki page.

This is pretty much everything you need to know to start with OSM and Overpass QL.

At this point, I recommend going to OSM homepage and exploring something on the map. It works pretty much like any other map site, like Google Maps or similar. However, one interesting thing you can do is clicking with a right mouse button, then you will get this menu:

OSM right click menu.

An interesting menu item is the Query features one. When you click it, you will get info on the left about what that thing on the map is, what objects there are:

OSM tags after clicking Query features
OSM tags after clicking Query features.

Finally you can choose one of the objects and see its tags as well:

Tags associated with an OSM object
Tags associated with an OSM object.

Basics of Overpass QL

At this point you are familiar with OSM, so why not move on to the query language Overpass offers.

There are actually two ways you can use the service. Either you use their API, or a web-based service Overpass Turbo. I’ll use Overpass Turbo for demonstration here but the query language is the same (unless you use some features specific to the web-based app; if I do, I’ll mention it).

When I go to Overpass Turbo homepage, I can see the page is divided into two parts — left side where you can type your queries, and right side where you can display results on the queries:

Overpass Turbo site.

Let’s see some basic queries, for example:

node[amenity=atm]({{bbox}});
out;

To translate this into a human language, it’d be:

Give me all nodes that have a tag amenity=atm and are within the map I can see on the right side.

That clumsy expression “within the map I can see on the right side” actually means “within a bounding box represented by the map on the right side”. If you zoom out, you’ll see more and the bounding box will be bigger. Also, {{bbox}}bounding box is available only in the web app, not in the Overpass API. This bounding box basically takes coordinates from the map on the right side in the web app. Obviously that’s not available in the API, so you have to define bounding box yourself.

ATMs returned by our first query.

At this point, you probably need to know what tags you can use to filter things.

You can see a list of them on a Wiki page. You can search for tags in a database:

Searching for tags in a OSM database
Searching for tags in a OSM database.

Typically if you have something to find in mind, you’d do the following:

  • search in the OSM database and find out what tags exist for what you want to search
  • try to find the tag value on the wiki page
  • build your query

An example could be the following situation. I want to search for schools.

  • I go to the database site and search for “school”, I get these results:
Search results for “school”.
  • I search for school on the Wiki page and find out that there are again a bunch of options, e.g. school as a building, driving school, music school, etc.
  • Now I build my query. It might look something like this:
(
node[amenity=school]({{bbox}});
node[building=school]({{bbox}});
node[amenity=music_school]({{bbox}});
);
out;

Then I get results back that contain schools, music schools, and schools as buildings.

A good question now would be why I put brackets around the 3 statements?

The reason is that by default results are assigned to a default set. The first statement does it, then the second statements does it and overwrites the content of the default set. If I write:

node[amenity=school]({{bbox}});
node[building=school]({{bbox}});
node[amenity=music_school]({{bbox}});
out;

I’ll get only music schools.

Actually, the default set is represented by ._ , so the following code is the same as the first one:

(
node[amenity=school]({{bbox}})->._;
node[building=school]({{bbox}})->._;
node[amenity=music_school]({{bbox}})->._;
)->._;
out;

Therefore -> is used to assign to a set.

This example pretty much explained logical OR. How about if I want to search for nodes that have multiple tags?

Then you’d put such conditions one after the other into square brackets:

node[highway=bus_stop][shelter=yes]({{bbox}});
out;

This returns bus stops that are also covered (with some kind of roof etc.).

A couple more things that are useful to know at this points.

If you want to search for a tag value that has spaces in it, you need to use quotes, for example:

node[highway=bus_stop][name="Nazionale/Quattro Fontane"]({{bbox}});
out;

If you work with some other programming language, or some other query language, chances are you are well familiar with this.

Another thing is, you can also use regex. The previous example using regex that will find names ending with “Fontane” would look like this:

node[highway=bus_stop][name~"Fontane$"]({{bbox}});
out;

This is mainly useful if you want to search for names.

You can also perform a case-insensitive search:

node[highway=bus_stop][name~"Fontane$", i]({{bbox}});
out;

There are more filter options. Another useful one might be to search for nodes that do not have a certain tag. You would prepend ! to the filter:

node[!highway]({{bbox}});
out;

You can find more in the docs in the filter section.

Two more things that should go into the basics section.

When a query finishes, the map on the right side does not update. But you can click this button to focus on the results:

Go to results button
Go to results button.

And finally I should mention settings. There’s a default timeout but some queries run longer. However, you can adjust the timeout in the setting section like so:

[timeout:120];area["name:en"=Prague]->.a;
node(area.a)[amenity=atm];
out;

I can change other settings, like the data format to JSON:

[timeout:120][out:json];area["name:en"=Prague]->.a;
node(area.a)[amenity=atm];
out;

Notice that the setting section has only one ; at the end. That’s how you can distinguish it from section with statements.

And in case you are wondering where you get that JSON, have a look at these buttons:

Buttons for switching between raw data and interactive map.

Areas

In all of the previous examples, I used {{bbox}} bounding box. But that’s not ideal because:

  • It’s available only in the web app.
  • It doesn’t apply well if I want to query an area that does not fit well into the rectangle map in the web app.

For that purpose, there’s something called an area. Typically, you’d find an area by its name:

area["name:en"=Prague]->.a;
node(area.a)[amenity=atm];
out;

This would find all ATMs in Prague. Notice two things:

  • I used name:en tag key because I suspect that name key would contain the value Praha, not the English name.
  • I assigned the result or the area query to a named set; I recommend using named sets, the code becomes a bit more clear.

So first I use an area query and then I filter nodes only in that area using an area filter.

A similar way would be to find a relation that represents a city boundary and then mapping it onto an area:

rel["name:en"=Prague][boundary=administrative]->.prague_rel;
.prague_rel map_to_area->.prague;
node(area.prague)[amenity=atm];
out;

These are two ways of working with areas. Chances are you’ll need to restrict your search to a geographical area, so this is something that will really become useful to you.

Examples

It’s time for some examples :)

In the Czech Republic, we use various voltages in power lines. The highest voltage is 400 kV. Let’s find where they go:

area["name:en"="Czechia"]->.cze;
way(area.cze)[power=line][voltage=400000];
(._;>;);
out;

A few things to mention at this points:

  • I’m looking for power lines which will be represented as ways in OSM.
  • This strange looking (._;>;); statement exists because I need to find nodes for the ways that the previous statement assigned into the default set. This is what the recurse down query does. ._ means take what’s in the default set. And the outcome will again be assigned to the default set.
400 kV power lines in the Czech Republic.

How about finding where tram number 3 and 9 go in Prague? It’s a simple query:

area["name:en"="Prague"]->.prague;
(
rel(area.prague)[route=tram][ref=3];
rel(area.prague)[route=tram][ref=9];
);
(._;>>;);
out;

Now I used recurse down relations query because I need to get nodes from relations, not only from ways.

The results looks like this on a map:

Tram routes 3 and 9 in Prague.

Around filter

Let’s consider the following situation: I want to find tram stops that are close to train stops. Typing such queries is possible with the aroundquery filter:

area["name:en"="Prague"]->.prague;
node(area.prague)[railway=tram_stop]->.trams;
(
node(around.trams:500)[railway=stop][!subway];
node(around.trams:500)[railway=station][!subway];
);
out;

On line 1 I find area that represents Prague and assign the result into a named set called .prague. On line 2 I find all tram stops in Prague and save the result into a named set called .trams. On line 3, 4, 5, and 6 I find all railway stops, but not subway stops that also have tag railway=stop, that are within 500 meters from the tram stops found on line 2. On line 7 I output what’s inside the default set, which is the result of the statement on line 3, 4, 5, and 6.

This looks like so on a map:

Train stops in Prague that are within 500 meters from a tram stop.

nw, nr, wr, nwr

Let’s see another example. I’ll find all things related to nuclear power in the Czech Republic:

area["name:en"="Czechia"]->.a;(
nwr(area.a)["plant:source"=nuclear];
nwr(area.a)["landfill:waste"=nuclear];
nwr(area.a)["generator:source"=nuclear];
nwr(area.a)["hazard"=nuclear];
nwr(area.a)["disused:generator:source"=nuclear];
nwr(area.a)["military"="nuclear_explosion_site"];
);
(._;>>;);
out meta;

(If you are wondering how I came up with all the tags, go to the the OSM database search page and search for nuclear.)

What I want to demonstrate here is the nwr query. nwr stands for node, way, relation and such a query returns either one of these objects.

nwr["plant:source"=nuclear];

This will find an object of type node, or way, or relation that has plant:sourse=nuclear tag. nwr can shorten the code because otherwise I’d have to write:

(
node["plant:source"=nuclear];
way["plant:source"=nuclear];
relation["plant:source"=nuclear];
);

So instead of 3 (or 5) lines, I can type only one.

You can type any combination of at least two different objects like this:

  • nw stands for node and way
  • nr stands for node and relation
  • wr stands for way and relation
  • nwr stands for node, way, and relation

However, you can’t type only n. The only abbreviation there is for a single object is rel that stands for relation.

How is all this useful?

I’ll finish my Overpass QL intro here but let’s answer one more question. All this is nice but how exactly is all this useful?

Apart from perhaps using this once a year when you go on holidays to an unknown place, when will you use it?

I’ve noticed OSM and Overpass QL are quite often used within the OSINT community when they need to geolocate an unknown place. Let’s consider there is a picture with a few pieces of information, e.g. a train station building next to a factory chimney. Then if you know at least a country or region, it might be possible to narrow down the number of possible places by running such queries (aroundquery would likely be used in this case).

Actually, if you participate in the OSINT quiz on Twitter, you might need to use OSM and Overpass QL sooner or later.

--

--