Skip to content
← Back to Workflows
workflowsBeginner· 8 min read

How to Download OpenStreetMap Data: QuickOSM and the Overpass API

Get roads, buildings, hospitals and other features from OpenStreetMap for your study area, either with the QuickOSM plugin in QGIS or with a short Python script, and save them as a GeoPackage you can use anywhere.

This guide has 2 routes. Pick one, or read both. Each route is complete on its own.

OpenStreetMap, usually shortened to OSM, is a map of the world that volunteers build and anyone may use. It is often the best free source of roads, buildings, hospitals, schools, rivers and land use for a district, and it is the quickest way to get a base layer for a project.

This guide shows two routes to the same data. Route A uses QuickOSM, a plugin that adds a form to QGIS. Route B uses a short Python script. In both you say what you want with a key and a value, and where you want it with an area.

How OSM describes a feature

Every feature in OSM has tags, and a tag is a key and a value. A hospital has the key amenity and the value hospital, written amenity=hospital. A main road has highway=primary. A building has building=yes. A river has waterway=river. The OSM wiki page Map features lists the common ones, and it is worth keeping open when you plan a query.

A feature can also be one of three shapes. A node is a point, such as a shop. A way is a line, such as a road, or a closed line that makes a polygon, such as a building. A relation groups other features, for example a large multipolygon lake with an island. When you download, you get up to three layers, one for each shape, because the same tag can appear on a point and on a polygon. A hospital might be mapped as a point in one town and as a building outline in the next.

Licence

OSM data is open under the Open Database Licence (ODbL). You may use it freely, including commercially, as long as you credit OpenStreetMap contributors and share any changed version of the database under the same licence. For a map or a report, write "© OpenStreetMap contributors" near the figure. The copyright page of OSM, linked in the sources, gives the exact wording.

Route A: QuickOSM in QGIS

  1. Install QuickOSM. Open Plugins > Manage and Install Plugins, search for QuickOSM, and click Install Plugin.
  2. Open Vector > QuickOSM > QuickOSM.
  3. The first time, a message about the OSM licence appears. Click I understand the copyrights, access to the plugin.
  4. On the Quick query tab, type the Key amenity and the Value hospital.
  5. Choose the area. In lets you type a place name such as Jagraon. Canvas Extent uses the part of the map that you see on the screen, which is the better choice for a district that has no clear place name, or when you want a particular box.
  6. Click Run query.

QuickOSM adds up to three layers to the project, one for points, one for lines and one for multipolygons, depending on how the features are mapped in your area. Layers with no features are not added. Open the attribute table to see the tags, such as name. The step-by-step version with screenshots is in the QGIS tutorial on capturing data.

To keep the result, right-click the layer, choose Export > Save Features As, and pick GeoPackage.

If you leave the Value empty, QuickOSM downloads every feature that has the key. building with no value gives every building, and that can be a very large download for a city. Start with a small area.

Route B: Python and the Overpass API

Overpass is the service that answers questions about OSM data. You send it a query and it sends back the features. A query has four parts: the output format, the kind of feature, the tag, and the box.

You need Python with requests, geopandas and shapely.

import requests

# Step 1: the service address, and a name for your script. Overpass refuses requests that do not say who is asking.
OVERPASS = "https://overpass-api.de/api/interpreter"
HEADERS = {"User-Agent": "timeandspatial-guide/1.0 (write your email address here)"}

def overpass(query):
    # Step 2: send the query and return the list of features from the answer
    reply = requests.post(OVERPASS, data={"data": query}, headers=HEADERS, timeout=120)
    reply.raise_for_status()
    return reply.json()["elements"]

# Step 3: the box. Overpass wants south, west, north, east, which is a different order from many other tools.
south, west, north, east = 30.74, 75.40, 30.84, 75.55         # a box of about 14 km by 11 km near Jagraon, Punjab

# Step 4: every hospital in the box, as a point. Features mapped as areas are reduced to their centre.
query = f'[out:json][timeout:60]; nwr["amenity"="hospital"]({south},{west},{north},{east}); out center;'
hospitals = overpass(query)
print(len(hospitals), "hospitals found")
print(hospitals[0]["type"], "|", hospitals[0]["tags"].get("name"))
33 hospitals found
node | Gupta Hospital, Kothe Baggu

Read the query piece by piece. [out:json] asks for the answer as JSON. nwr means nodes, ways and relations, so all three shapes. ["amenity"="hospital"] is the tag. The four numbers are the box. out center; asks for one point for every feature, which suits a list of places. The Overpass wiki page in the sources lists everything else you can ask.

Overpass is a shared service. The rules are: say who you are, ask for small areas, and do not send the same query many times a minute. A request that is too large or too frequent is refused with an error 429 or a timeout.

Turn the answer into a GeoPackage

import geopandas as gpd

# Step 1: build one row for each feature, with a point and its name and type
rows = []
for item in hospitals:
    lat = item.get("lat") or item["center"]["lat"]         # nodes carry lat and lon, areas carry a centre
    lon = item.get("lon") or item["center"]["lon"]
    tags = item.get("tags", {})
    rows.append({"osm_type": item["type"], "osm_id": item["id"], "name": tags.get("name"), "operator": tags.get("operator"), "lon": lon, "lat": lat})

# Step 2: a GeoDataFrame, which is a table with a geometry column. OSM coordinates are longitude and latitude, EPSG:4326.
hospital_points = gpd.GeoDataFrame(rows, geometry=gpd.points_from_xy([r["lon"] for r in rows], [r["lat"] for r in rows]), crs="EPSG:4326")
print(hospital_points[["osm_type", "name"]].head())

# Step 3: save the layer into a GeoPackage
hospital_points.to_file("jagraon_osm.gpkg", layer="hospitals", driver="GPKG")
  osm_type                                        name
0     node                 Gupta Hospital, Kothe Baggu
1     node                               Jain Hospital
2     node              Ayush Speech and Autism Centre
3     node  Karan Singla Surgical and General Hospital
4     node                             Sharma Hospital

Roads: lines instead of points

Roads are ways, so ask for their geometry. The query asks for the main road classes only. Asking for every highway returns tracks and footpaths too, and the answer is much larger.

from shapely.geometry import LineString

# Step 1: main roads in the box, as lines. "out geom" returns the coordinates of every vertex.
road_classes = "motorway|trunk|primary|secondary|tertiary"
query = f'[out:json][timeout:60]; way["highway"~"^({road_classes})$"]({south},{west},{north},{east}); out geom;'
ways = overpass(query)
print(len(ways), "road segments found")

# Step 2: turn each way into a line. A way carries its vertices as a list of points.
roads = gpd.GeoDataFrame(
    [{"osm_id": w["id"], "highway": w["tags"]["highway"], "name": w["tags"].get("name")} for w in ways],
    geometry=[LineString([(p["lon"], p["lat"]) for p in w["geometry"]]) for w in ways],
    crs="EPSG:4326",
)

# Step 3: check the result. Reproject to the UTM zone of the place (metres) so that lengths mean something.
roads_utm = roads.to_crs(32643)
roads["length_km"] = roads_utm.length / 1000
print(roads.groupby("highway")["length_km"].sum().round(1))

# Step 4: save the layer next to the hospitals
roads.to_file("jagraon_osm.gpkg", layer="roads", driver="GPKG")
190 road segments found
highway
secondary     40.9
tertiary     203.5
trunk         36.8
Name: length_km, dtype: float64

A length in degrees is meaningless, which is why the check reprojects first. Always measure length and area in a projected CRS. The same file now holds two layers, hospitals and roads. QGIS shows both when you open it.

How complete is the data?

OSM is as good as its volunteers. Main roads in a district are usually mapped well. Buildings, small shops and land use vary a lot from place to place. A hospital that is missing from the layer is not proof that it does not exist, and a layer with many features is not proof of a good survey. Compare a count with an official source when it matters, and say in your report that OSM is the source and when you downloaded it, because the map changes every day.

Common mistakes

  • Giving Overpass the box in the wrong order. It wants south, west, north, east, and many tools use west, south, east, north.
  • Leaving out the User-Agent and getting a refusal.
  • Asking for a whole city's buildings in one query and getting a timeout. Cut the area into pieces.
  • Forgetting that a feature can come back as a point, a line or a polygon, and keeping only one layer.
  • Measuring length in degrees.
  • Using the data without the credit "© OpenStreetMap contributors".

Key takeaways