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 January 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())
81,649 highway ways inside D.C., 6,356 km
highway
footway         2472
service         1439
residential     1042
tertiary         277
primary          265
secondary        211
cycleway         190
path              94
motorway          75
unclassified      65

Inside the District, 81,649 highway ways add up to 6,356 km. Footways are the longest class by far, 2,472 km, almost as long as service roads (1,439 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,356 km of highway ways.

Group the classes, the longest streets, detail completeness

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': 2814, 'service': 1439, 'minor road': 1389, 'major road': 675, 'other': 39}

Grouped, paths account for 2,814 km, service roads 1,439 km, minor roads 1,389 km and major roads 675 km.

Length of OpenStreetMap highways in Washington, D.C., by group. km of highway ways by grouppaths, footways, cycleways2,814 kmservice roads1,439 kmminor roads1,389 kmmajor roads675 km01000 km2000 km3000 km6,356 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,901
name
Anacostia Freeway                 24.3
16th Street Northwest             16.7
14th Street Northwest             11.7
New York Avenue Northeast         11.6
Massachusetts Avenue Northwest    11.2
Connecticut Avenue Northwest       9.9

The 1,901 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 50% of major-road length
    lanes: on 99% of major-road length
   oneway: on 67% of major-road length
maxspeed values: {'25 mph': 1400, '30 mph': 574, '40 mph': 200, '35 mph': 164, '50 mph': 115}

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

Dual carriageways in D.C.

The data used on this page

The Geofabrik OpenStreetMap extract for the District of Columbia dated 1 January 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-260101.osm.pbf", "https://download.geofabrik.de/north-america/us/district-of-columbia-260101.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: 62%
motorway and trunk: 118 km of ways, of which one-way 98 km

62% of the major-road length is one-way. Of the 118 km of motorway and trunk ways, 98 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.

The highway key describes a role, not a size

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 -1maxspeed50%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.

Pedestrian areas, proposed roads and speeds

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 classes81,649 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.