How to Get Building Heights and Levels From OpenStreetMap in Python

On this page

Problem statement

Building footprints are only half of a city: 3D maps, floor-area estimates, solar and shadow models all need heights. OpenStreetMap records them with two tags, height in metres and building:levels for the number of floors above ground, plus roof:levels and min_height for details. Neither is required. Their values come as strings, sometimes with units, and coverage varies from street to street, so using them takes parsing, checking and a fallback for the buildings that have neither.

This page extracts every building in Washington, D.C. from the OpenStreetMap extract of 1 January 2026. It parses height and building:levels, measures how many buildings have each, estimates metres per level where both are present, finds the tallest buildings, and estimates total floor area.

Quick answer

Read the buildings with their height tags and parse the strings to numbers:

fp = (osmium.FileProcessor(str(PBF)).with_areas()
      .with_filter(osmium.filter.KeyFilter("building"))
      .with_filter(osmium.filter.GeoInterfaceFilter(tags=["building", "name", "height", "building:levels", "roof:levels"])))
B = gpd.GeoDataFrame.from_features(fp, crs=4326)
B = B[B.geom_type.isin(["Polygon", "MultiPolygon"])]
B = B[B.representative_point().within(DC)].to_crs(32618).reset_index(drop=True)
B["h"] = pd.to_numeric(B.height.str.extract(r"^\s*([0-9.]+)\s*(?:m)?\s*$")[0], errors="coerce")
B["lv"] = pd.to_numeric(B["building:levels"], errors="coerce")
print(f"buildings inside D.C.: {len(B):,}; with height: {B.h.notna().mean():.1%}; with building:levels: {B.lv.notna().mean():.1%}; with either: {(B.h.notna() | B.lv.notna()).mean():.1%}")
buildings inside D.C.: 161,354; with height: 0.2%; with building:levels: 36.6%; with either: 36.6%

The District has 161,354 building polygons in OSM. Only 0.2% carry a height, while 36.6% have building:levels, so together 36.6% have one or the other.

Map of 2,276 building footprints in a 3 by 3 km window of central D.C., the 34% with building:levels coloured from dark for one level to yellow for twelve, and the rest grey. Map of 2,276 building footprints in a 3 by 3 km window of central D.C., the 34% with building:levels coloured from dark for one level to yellow for twelve, and the rest grey.
Which buildings have levels.

Non-numeric values, metres per level, floor area

1. Values that are not plain numbers

height should be in metres without a unit, but other forms appear. Count the values the parser rejected:

raw = B.height.dropna()
bad = raw[B.loc[raw.index, "h"].isna()]
print(f"height values: {len(raw):,}; not a plain number in metres: {len(bad)}; examples {bad.value_counts().head(5).to_dict()}")
lv_raw = B["building:levels"].dropna()
print(f"building:levels values: {len(lv_raw):,}; not a number: {B.loc[lv_raw.index, 'lv'].isna().sum()}")
height values: 340; not a plain number in metres: 2; examples {"166.5'": 1, "130.0'": 1}
building:levels values: 59,044; not a number: 1

Of the 340 height values, only 2 are not plain metres — 166.5' and 130.0', feet written with a prime — and only one of the 59,044 building:levels values fails to parse. Values this clean are typical of imported data; the two in feet are the kind of edit that arrives later, one mapper at a time.

Number of buildings in D.C. by building:levels value, from OpenStreetMap. buildings by levels13,7821.52,330247,9532.51,62231,674025k50k102,310 of 161,354 buildings have no building:levels spatialworkflow.io
Most tagged buildings have two levels.

2. Metres per level

Where both tags exist, their ratio gives a storey height to fill in buildings that have only levels:

both = B[B.h.notna() & B.lv.notna() & (B.lv > 0)]
mpl = both.h / both.lv
print(f"buildings with both: {len(both):,}; metres per level: median {mpl.median():.2f}, middle half {mpl.quantile(0.25):.2f} to {mpl.quantile(0.75):.2f}")
print("levels distribution:", B.lv.value_counts().sort_index().head(8).astype(int).to_dict())
buildings with both: 248; metres per level: median 3.62, middle half 3.62 to 3.62
levels distribution: {0.0: 3, 1.0: 3782, 1.5: 2330, 2.0: 47953, 2.5: 1622, 3.0: 1674, 3.5: 12, 4.0: 407}

The 248 buildings with both tags give exactly 3.62 m per level across the whole middle half, which suggests their heights were computed from levels with a fixed factor rather than measured. Levels themselves cluster at 2 (47,953 buildings), with many half-levels such as 1.5 and 2.5.

3. Floor area

Footprint area times levels estimates gross floor area. Fill missing levels from height, then report how much is covered:

B["lv_est"] = B.lv.fillna((B.h / mpl.median()).round())
B["floor_m2"] = B.area * B.lv_est
covered = B.lv_est.notna()
print(f"footprint area: {B.area.sum() / 1e6:.1f} km²; floor area of buildings with levels: {B.floor_m2.sum() / 1e6:.1f} km² ({covered.mean():.0%} of buildings, {B.loc[covered].area.sum() / B.area.sum():.0%} of footprint area)")
footprint area: 25.7 km²; floor area of buildings with levels: 39.1 km² (37% of buildings, 32% of footprint area)

Buildings cover 25.7 km² of the District. The 37% of buildings with levels, 32% of the footprint area, add up to 39.1 km² of floor area.

The tallest buildings

The data used on this page

The Geofabrik OpenStreetMap extract for the District of Columbia dated 1 January 2026 (© OpenStreetMap contributors, ODbL), and the District's boundary from the US Census Bureau's 2023 cartographic boundary files at 1:500,000. Areas are in UTM zone 18N.

from pathlib import Path

import geopandas as gpd
import osmium
import pandas as pd
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).geometry.iloc[0]
print("boundary loaded")
boundary loaded

The District boundary is loaded for filtering.

Example 1: the tallest buildings

top = B.nlargest(6, "h")[["name", "building", "h", "lv"]]
print(top.to_string(index=False))
print(f"buildings taller than 40 m: {(B.h > 40).sum()}; median height of tagged buildings: {B.h.median():.1f} m")
                 name   building        h   lv
  Washington Monument      civic 169.2940  NaN
United States Capitol government  87.6000  3.0
           Healy Hall university  61.0000  NaN
                  NaN        yes  60.5900 13.0
                  NaN        yes  57.3024 15.0
          Camden NoMa apartments  54.3800 15.0
buildings taller than 40 m: 95; median height of tagged buildings: 32.6 m

The tallest tagged building is the Washington Monument at 169.3 m, followed by the Capitol at 87.6 m and Georgetown's Healy Hall at 61.0 m. 95 buildings are tagged taller than 40 m, and the median tagged height is 32.6 m, which says more about which buildings got a height tag than about the city's buildings.

The tallest buildings in D.C. by OpenStreetMap height tag. tallest by height tagbuildingtypeheightlevelsWashington Monumentcivic169.3 m-United States Capitolgovernment87.6 m3Healy Halluniversity61.0 m-(unnamed)yes60.6 m13(unnamed)yes57.3 m15only 340 of 161,354 buildings have a height tagOSM D.C. extract, 1 January 2026 spatialworkflow.io
The tallest tagged buildings.

The Simple 3D Buildings scheme

The 3D building tags in OpenStreetMap. Simple 3D Buildings tagsroof:levelsfloors in the roofnot in building:levelsheightground to roof top, metres0.2% in D.C.building:levelsfloors above ground36.6% in D.C.min_heightparts that start above groundbridges, overhangsAll values are strings spatialworkflow.io
The 3D building tags.

OpenStreetMap's 3D building tags follow the Simple 3D Buildings scheme. height is the height from the ground to the top of the roof in metres. building:levels counts floors above ground, excluding roof levels, which go in roof:levels. min_height and building:min_level describe parts that start above the ground, such as bridges between buildings. Complex buildings are split into building:part features, each with its own height. Values are strings. Plain metres are the convention, but units, ranges and typos turn up, so a parser has to accept the clean form and report the rest rather than silently converting it.

Coverage is the real limit. Height tags arrive when a mapper adds them or when an import brings them. In D.C., building:levels is on more than a third of buildings, while height is on almost none, so levels carry nearly all the height information. Where they do not, a storey height derived from buildings with both tags fills in levels, and footprint area times levels gives floor area. Those are estimates, and they should be reported with the share of buildings or footprint area they cover. For complete height coverage, lidar-derived heights or Overture's buildings complement OSM.

Building parts, underground levels and feet

Building height data from OpenStreetMap: traps and fixes. height trapsSymptomFixheights as textparse, report rejectsmost buildings blanklevels x storey heightheight from levels alreadycheck for a fixed ratioparts and outline bothcount building:part once248 buildings give exactly 3.62 m per level spatialworkflow.io
Height traps.
  • building:part: a building with parts may carry the overall shape on the outline and heights on the parts; avoid counting both.
  • Underground levels: building:levels:underground counts basements, not included in building:levels.
  • Feet: values like 85' occur in the US; convert them explicitly rather than dropping them.
  • 3D maps: extrude by height, or by levels times a storey height, and say which.

FAQ

How do I get building heights from OpenStreetMap?

Read the height tag in metres and building:levels for floors; parse both from strings, since neither is guaranteed to be a clean number.

How many buildings in OSM have heights?

It varies by city; in D.C. a minority of buildings have either tag, so measure coverage before using them.

How do I convert building levels to height?

Multiply by a storey height, ideally derived from local buildings that have both tags; 3 to 4 m per level is typical, and the D.C. tags imply 3.62 m.

What is the difference between height and building:levels?

height is metres to the top of the roof; building:levels counts floors above ground, excluding the roof.