How to Get Roads by Type From OpenStreetMap in Python

On this page

Problem statement

Every road in OpenStreetMap carries a highway tag that says what kind of road it is, from motorway down to residential, service and footway. That makes OSM the easiest open source for questions like "how many kilometres of each road class does this city have?" or "where are the main roads?". The answers depend on details that are easy to miss. Footpaths and service roads are highways too. Dual carriageways are mapped as two ways. Lengths have to be measured in a projected CRS, and a regional extract reaches beyond the city's boundary.

This page extracts every highway in Washington, D.C. from the OpenStreetMap extract of 1 October 2026. It measures length by class inside the District's boundary, groups the classes into a usable hierarchy, finds the longest named streets, and checks how often the maxspeed and lanes tags are filled in.

Quick answer

Read the highway ways as lines, clip to the boundary, and sum lengths in metres:

fp = (osmium.FileProcessor(str(PBF)).with_locations()
      .with_filter(osmium.filter.EntityFilter(osmium.osm.WAY))
      .with_filter(osmium.filter.KeyFilter("highway"))
      .with_filter(osmium.filter.GeoInterfaceFilter(tags=["highway", "name", "maxspeed", "lanes", "oneway"])))
H = gpd.GeoDataFrame.from_features(fp, crs=4326)
H = gpd.clip(H[H.geom_type == "LineString"], DC).to_crs(32618)
H["km"] = H.length / 1000
km = H.groupby("highway").km.sum().sort_values(ascending=False)
print(f"{len(H):,} highway ways inside D.C., {H.km.sum():,.0f} km")
print(km.head(10).round(0).astype(int).to_string())
93,562 highway ways inside D.C., 6,918 km
highway
footway         3017
service         1448
residential     1042
tertiary         278
primary          251
secondary        218
cycleway         192
path              90
motorway          75
unclassified      70

Inside the District, 93,562 highway ways add up to 6,918 km. Footways are the longest class by far, 3,017 km, more than service roads (1,448 km) and residential streets (1,042 km) together, because D.C.'s sidewalks are mapped as separate ways.

Map of all OSM highways inside D.C.: major roads in red radiating from downtown, minor roads in blue filling the grid, with service roads and paths underneath. Map of all OSM highways inside D.C.: major roads in red radiating from downtown, minor roads in blue filling the grid, with service roads and paths underneath.
6,918 km of highway ways.

Step-by-step solution

1. Group the classes

Thirty-odd highway values collapse into a few groups for mapping and statistics:

GROUP = {"motorway": "major road", "trunk": "major road", "primary": "major road", "secondary": "major road",
         "tertiary": "minor road", "residential": "minor road", "unclassified": "minor road", "living_street": "minor road",
         "service": "service", "footway": "path", "path": "path", "pedestrian": "path", "steps": "path", "cycleway": "path", "track": "path"}
H["group"] = H.highway.str.replace("_link", "", regex=False).map(GROUP).fillna("other")
print(H.groupby("group").km.sum().sort_values(ascending=False).round(0).astype(int).to_dict())
{'path': 3359, 'service': 1448, 'minor road': 1396, 'major road': 676, 'other': 38}

Grouped, paths account for 3,359 km, service roads 1,448 km, minor roads 1,396 km and major roads 676 km.

Length of OpenStreetMap highways in Washington, D.C., by group. km of highway ways by grouppaths, footways, cycleways3,359 kmservice roads1,448 kmminor roads1,396 kmmajor roads676 km01000 km2000 km3000 km6,918 km in all, measured in UTM 18N spatialworkflow.io
Paths outrank streets by length.

2. The longest named streets

A street is many ways, split wherever a tag changes. Sum by name:

streets = H[H.group.isin(["major road", "minor road"]) & H.name.notna()].groupby("name").km.sum().sort_values(ascending=False)
print(f"named streets: {len(streets):,}")
print(streets.head(6).round(1).to_string())
named streets: 1,909
name
Anacostia Freeway                 24.3
16th Street Northwest             16.7
New York Avenue Northeast         11.7
14th Street Northwest             11.7
Massachusetts Avenue Northwest    11.2
Connecticut Avenue Northwest       9.9

The 1,909 named streets are led by the Anacostia Freeway at 24.3 km, a figure that counts both carriageways, and 16th Street Northwest at 16.7 km.

3. How complete the details are

maxspeed and lanes are optional. Measure their coverage by length on the major roads:

major = H[H.group == "major road"]
for tag in ("maxspeed", "lanes", "oneway"):
    print(f"{tag:>9}: on {major.loc[major[tag].notna(), 'km'].sum() / major.km.sum():.0%} of major-road length")
print("maxspeed values:", major.maxspeed.value_counts().head(5).to_dict())
 maxspeed: on 65% of major-road length
    lanes: on 99% of major-road length
   oneway: on 67% of major-road length
maxspeed values: {'25 mph': 2297, '30 mph': 740, '35 mph': 264, '40 mph': 226, '20 mph': 139}

lanes is on 99% of the major-road length and maxspeed on 65%, almost all of it as strings in miles per hour, 25 mph most often.

Code examples

The data used on this page

The Geofabrik OpenStreetMap extract for the District of Columbia dated 1 October 2026 (ยฉ OpenStreetMap contributors, ODbL), clipped to the District's boundary from the US Census Bureau's 2023 cartographic boundary files at 1:500,000. Lengths are measured in UTM zone 18N (EPSG:32618).

from pathlib import Path

import geopandas as gpd
import osmium
import requests

Path("data").mkdir(exist_ok=True)
def cached(path, url):
    p = Path(path)
    if not p.exists():
        r = requests.get(url, timeout=600)
        r.raise_for_status()
        p.write_bytes(r.content)
    return p
PBF = cached("data/district-of-columbia-261001.osm.pbf", "https://download.geofabrik.de/north-america/us/district-of-columbia-261001.osm.pbf")
C = gpd.read_file(cached("data/cb_2023_us_county_500k.zip", "https://www2.census.gov/geo/tiger/GENZ2023/shp/cb_2023_us_county_500k.zip"))
DC = C[C.GEOID == "11001"].to_crs(4326)
print(f"D.C.: {DC.to_crs(32618).area.iloc[0] / 1e6:.1f} kmยฒ")
D.C.: 176.9 kmยฒ

The District's boundary covers 176.9 kmยฒ, water included.

Example 1: dual carriageways

Divided roads are mapped as two one-way ways, one per direction, so their length counts twice. Measure how much of the major-road length is one-way:

ow = major.oneway.isin(["yes", "-1"])
print(f"one-way share of major-road length: {major.loc[ow, 'km'].sum() / major.km.sum():.0%}")
print(f"motorway and trunk: {H[H.highway.isin(['motorway', 'trunk'])].km.sum():.0f} km of ways, of which one-way {H[H.highway.isin(['motorway', 'trunk']) & H.oneway.isin(['yes', '-1'])].km.sum():.0f} km")
one-way share of major-road length: 63%
motorway and trunk: 125 km of ways, of which one-way 100 km

63% of the major-road length is one-way. Of the 125 km of motorway and trunk ways, 100 km are one-way carriageways, so the roads themselves are much shorter than the sum of their ways.

Map of D.C.'s major roads, one-way ways in dark blue totalling 423 km and two-way ways in light blue totalling 253 km, with the freeways mostly one-way. Map of D.C.'s major roads, one-way ways in dark blue totalling 423 km and two-way ways in light blue totalling 253 km, with the freeways mostly one-way.
Divided roads are mapped twice.

Explanation

Coverage of optional tags on the major roads of Washington, D.C. tag coverage, major-road lengthshare of lengthwhat it holdslanes99%number of lanesoneway67%yes, no or -1maxspeed65%strings, 25 mph most oftenMeasure coverage before using a tag spatialworkflow.io
Optional tags, measured.

The highway key describes a way's role in the network, not its physical size. motorway, trunk, primary, secondary and tertiary form the hierarchy of through roads. residential and unclassified are local streets. service covers driveways, parking aisles and alleys, and footway, cycleway, path and steps are for people on foot or bike. The _link suffix marks ramps and slip roads. In a dense city the non-car classes often add up to more length than the streets, because every sidewalk mapped as its own way counts.

Turning ways into kilometres needs three corrections. Lengths must be measured in metres in a projected CRS, here UTM 18N, not in degrees. The extract must be clipped to the area of interest, because a Geofabrik region includes a margin. And divided roads, mapped as one way per direction, count twice. That is right for lane-kilometres of carriageway, but wrong for "how long is this road". Attributes like maxspeed and lanes are added by mappers when they know them, so their coverage should be measured before they are used.

Edge cases or notes

Road statistics from OpenStreetMap: traps and fixes. road length trapsSymptomFixlengths in degreesproject to UTM firsttoo long: extract marginclip to the boundarydivided roads doubledmind oneway carriagewayssidewalks dominategroup the highway classes93,562 highway ways inside D.C. spatialworkflow.io
Road length traps.
  • Areas: highway=pedestrian squares can be polygons; read with with_areas() if plazas matter.
  • Construction and proposed roads: highway=construction and highway=proposed are not open roads; exclude them.
  • Routing: for a connected graph with nodes at intersections, use OSMnx rather than raw ways.
  • Speeds: maxspeed values are strings such as 25 mph; see parsing maxspeed.

FAQ

How do I get roads from OpenStreetMap by type?

Filter ways with the highway key and group by its value; motorway to tertiary are through roads, residential and unclassified local streets.

How do I calculate road length from OSM data?

Project the lines to a metric CRS such as UTM and sum geometry.length; clip to your boundary first.

Why are some roads counted twice?

Divided roads are mapped as two one-way ways, one for each direction.

What does highway=service mean?

Driveways, alleys, parking aisles and other access roads; usually excluded from street statistics.