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.

Map of the D.C. region: the District in blue, the only boundary assembled from the extract, inside a dashed red box marking the extract's bounds, with Maryland and Virginia drawn from Census boundaries. Map of the D.C. region: the District in blue, the only boundary assembled from the extract, inside a dashed red box marking the extract's bounds, with Maryland and Virginia drawn from Census boundaries.
Only the District can be built.

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.

Share of member ways present in the D.C. extract for boundary relations that could not be built. member ways present in the extractUnited States (1,703 ways)0%Fairfax County (92)2%Virginia (301)4%Maryland (364)17%District of Columbiabuilt025%50%75%100%27 boundary relations in the file, 9 built spatialworkflow.io
Members present in the file.

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.

Why a relation can be in an extract while its polygon cannot be built. present is not completeareaneeds closed ringsdropped silentlymember waysoutside the cutMaryland: 61 of 364relationin the file27 boundary relationsExtracts keep relations that touch the region spatialworkflow.io
Present is not complete.

How extracts treat features crossing the cut

Boundaries from a regional extract compared with a complete source. where to get boundariesregional extractβœ“boundaries wholly inside: builtβœ“D.C. and its 8 wardsβœ“neighbours: droppedcomplete sourceβœ“larger extract or smart clipβœ“boundary datasetsβœ“Census states: all threeCheck members before trusting a relation spatialworkflow.io
Where to get boundaries.

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

Missing polygons at extract edges: symptoms and fixes. edge lossesSymptomFixstate or county missingcomplete boundary sourcebig river cut offlarger extractrelation present, no areacount members presentroute partialsmart extract strategy3 of 72 water and park relations incomplete too spatialworkflow.io
Edge losses.
  • Clipping strategies: osmium-tool's extract offers simple, complete_ways and smart strategies; smart completes 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.

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.