Fixing Boundary Polygons Missing From an OpenStreetMap Extract
On this page
Problem statement
You extract administrative boundaries from a regional OpenStreetMap file, a Geofabrik state or city extract, and the polygons for the region's neighbours, its state or its country are not there, with no error. The relations are in the file. What is missing are their members: the ways that make up the rest of the boundary run outside the extract. A multipolygon that cannot be closed cannot be assembled, so the reader drops it silently.
This page reproduces the problem with the Washington, D.C. extract of 1 January 2026. It lists the boundary relations in the file, shows which ones pyosmium can and cannot build, measures how incomplete the missing ones are, and fixes it by getting boundaries from a source that contains them whole.
Quick answer
Compare the boundary relations in the file with the areas pyosmium builds:
con = duckdb.connect(); con.sql("INSTALL spatial; LOAD spatial")
rels = con.sql(f"""SELECT id, tags['name'] AS name, tags['admin_level'] AS admin_level, len(refs) AS members
FROM ST_ReadOSM('{PBF}') WHERE kind = 'relation' AND tags['boundary'] = 'administrative'""").df()
fp = (osmium.FileProcessor(str(PBF)).with_areas().with_filter(osmium.filter.EntityFilter(osmium.osm.AREA))
.with_filter(osmium.filter.TagFilter(("boundary", "administrative")))
.with_filter(osmium.filter.GeoInterfaceFilter(tags=["name", "admin_level"])))
built = gpd.GeoDataFrame.from_features(fp, crs=4326)
rels["built"] = rels.name.isin(built.name)
print(f"boundary relations in the file: {len(rels)}; assembled into areas: {rels.built.sum()}")
print(rels.sort_values("admin_level")[["name", "admin_level", "members", "built"]].head(12).to_string(index=False))
boundary relations in the file: 27; assembled into areas: 9
name admin_level members built
Jefferson Houston 11 74 False
Lyles-Crouch 11 32 False
United States 2 1706 False
District of Columbia 4 70 True
Maryland 4 367 False
Virginia 4 304 False
Alexandria 6 31 False
Prince George's County 6 126 False
Montgomery County 6 79 False
Fairfax County 6 94 False
Arlington County 6 31 False
Takoma Park 8 10 False
The file has 27 administrative boundary relations, but only 9 are assembled into areas. The District of Columbia is built, while Maryland, Virginia, the United States and the counties across the border are not.
Members outside the file, what survives, a complete source
1. Why they fail: members outside the file
A relation lists member ways by ID. Count how many of each relation's member ways are actually in the extract:
members = con.sql(f"""WITH r AS (SELECT id, tags['name'] AS name, unnest(refs) AS ref, unnest(ref_types) AS t
FROM ST_ReadOSM('{PBF}') WHERE kind = 'relation' AND tags['boundary'] = 'administrative'),
w AS (SELECT id FROM ST_ReadOSM('{PBF}') WHERE kind = 'way')
SELECT r.name, count(*) FILTER (WHERE r.t = 'way') AS way_members,
count(w.id) AS ways_present
FROM r LEFT JOIN w ON r.ref = w.id AND r.t = 'way' GROUP BY r.name""").df()
members["share_present"] = (members.ways_present / members.way_members).round(2)
print(members.sort_values("share_present").head(8).to_string(index=False))
name way_members ways_present share_present
United States 1703 0 0.00
Fairfax County 92 2 0.02
Virginia 301 11 0.04
Jefferson Houston 74 3 0.04
Lyles-Crouch 32 3 0.09
Alexandria 30 4 0.13
Maryland 364 61 0.17
Seat Pleasant 16 3 0.19
None of the United States relation's 1,703 member ways is in the file, and Virginia has 11 of 301. Maryland has 61 of 364, still far too few to close a ring.
2. What the reader keeps
Only boundaries entirely inside the extract survive: the District and its subdivisions.
print(f"areas built: {len(built)}; named: {built.name.notna().sum()}")
print(built.dropna(subset=["name"]).sort_values("admin_level")[["name", "admin_level"]].to_string(index=False))
areas built: 18; named: 9
name admin_level
District of Columbia 4
Ward 7 9
Ward 8 9
Ward 3 9
Ward 1 9
Ward 5 9
Ward 4 9
Ward 6 9
Ward 2 9
The reader builds 18 areas, of which 9 are named: the District itself and its eight wards, all of them entirely inside the extract.
3. The fix: boundaries from a complete source
For the boundaries of the region's neighbours, use a source that has them whole: a larger extract, or official boundaries. The Census cartographic boundary file has the states:
ST = gpd.read_file(cached("data/cb_2023_us_state_500k.zip", "https://www2.census.gov/geo/tiger/GENZ2023/shp/cb_2023_us_state_500k.zip"))
near = ST[ST.STUSPS.isin(["DC", "MD", "VA"])].to_crs(32618)
print(near.assign(km2=near.area / 1e6)[["NAME", "km2"]].round(0).to_string(index=False))
NAME km2
District of Columbia 177.0
Maryland 26636.0
Virginia 105075.0
The Census state boundaries provide all three whole: the District at 177 kmΒ², Maryland at 26,636 kmΒ² and Virginia at 105,075 kmΒ², water included.
Rivers and parks lose members too
The data used on this page
The Geofabrik OpenStreetMap extract for the District of Columbia dated 1 January 2026 (Β© OpenStreetMap contributors, ODbL), and the US Census Bureau's 2023 state cartographic boundaries at 1:500,000.
from pathlib import Path
import duckdb
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")
print(f"{PBF.name} ready")
district-of-columbia-260101.osm.pbf ready
The dated D.C. extract is ready.
Example 1: the same issue for rivers and parks
Any multipolygon that crosses the extract's edge has the same problem. Count water and park relations whose member ways are not all present:
inc = con.sql(f"""WITH r AS (SELECT id, unnest(refs) AS ref, unnest(ref_types) AS t FROM ST_ReadOSM('{PBF}')
WHERE kind = 'relation' AND (tags['natural'] = 'water' OR tags['leisure'] = 'park')),
w AS (SELECT id FROM ST_ReadOSM('{PBF}') WHERE kind = 'way')
SELECT r.id, count(*) FILTER (WHERE r.t = 'way') AS n, count(w.id) AS present
FROM r LEFT JOIN w ON r.ref = w.id AND r.t = 'way' GROUP BY r.id""").df()
print(f"water and park relations: {len(inc)}; with some member ways missing: {(inc.present < inc.n).sum()}")
water and park relations: 70; with some member ways missing: 3
Of 72 water and park relations, 3 have member ways missing, so those few polygons at the extract's edge would also be lost.
How extracts treat features crossing the cut
Regional extracts are cut from the planet with a polygon, and how they treat features crossing the cut decides what survives. The D.C. extract keeps relations that touch the region, but not every member of a large relation from the rest of the world: the United States boundary relation is in the file, but most of its member ways along the coasts and borders are not. Multipolygon assembly needs closed rings made from all the outer members. When some are missing the ring cannot close, and pyosmium, like most readers, skips the relation without raising an error.
So a relation's presence in a file says nothing about whether its geometry can be built. For anything that extends beyond the extract, such as states, counties across the border, long rivers or large parks on the edge, check members against the ways present. Then use a source that contains the feature completely: a larger extract, a boundary-specific service, or official boundaries. For administrative units, official boundary files are usually the better source anyway, because their lines carry legal status and stable codes.
Clipping strategies and silent skips
- Clipping strategies: osmium-tool's
extractofferssimple,complete_waysandsmartstrategies;smartcompletes multipolygons, at the cost of a larger file. - Boundary datasets: OSM-derived boundary collections such as osm-boundaries.com or geoBoundaries provide complete polygons.
- Silent skips: pyosmium can report broken areas with an area assembler callback; most simple scripts never see them.
- Same for routes: route relations crossing the edge are partial too.
Internal links
- How to download administrative boundaries in Python β complete boundaries
- Fixing OSM lakes that show as lines β area assembly in general
- How to read an OpenStreetMap PBF file in Python β reading the extract
FAQ
Why are some boundaries missing from my OSM extract?
Their relations reference ways outside the extract, so the polygon cannot be closed and the reader drops it without an error.
How do I check if an OSM relation is complete?
Compare each relation's member way IDs with the ways present in the file.
How do I get complete boundaries from OSM?
Use a larger extract, an extract made with a smart strategy, or a boundary dataset derived from the full planet.
Does this affect other features?
Yes, any multipolygon or route relation crossing the extract's edge, such as long rivers or parks.