Fixing OpenStreetMap Features Counted Twice as Lines and Polygons
On this page
Problem statement
You read buildings or amenities from an OpenStreetMap file with areas enabled, count them, and get far more than expected. The duplicates are not in the data: they come from how the reader assembles geometry. A closed way, a building outline or a car park, is both a way and an area. pyosmium's with_areas() emits it twice, once as a LineString for the way and once as a MultiPolygon for the area, and any count, map or join that does not drop one of them doubles those features.
This page reproduces the doubling on the Washington, D.C. OpenStreetMap extract of 1 January 2026, measures how many features it adds, and fixes it two ways: by filtering geometry types after reading, and by asking pyosmium for nodes and areas only.
Quick answer
Keep nodes and areas, and drop the way copies, with an entity filter:
fp = (osmium.FileProcessor(str(PBF)).with_areas()
.with_filter(osmium.filter.EntityFilter(osmium.osm.NODE | osmium.osm.AREA))
.with_filter(osmium.filter.KeyFilter("amenity"))
.with_filter(osmium.filter.GeoInterfaceFilter(tags=["amenity"])))
fixed = gpd.GeoDataFrame.from_features(fp, crs=4326)
print(f"amenity features with nodes and areas only: {len(fixed):,}; {fixed.geom_type.value_counts().to_dict()}")
amenity features with nodes and areas only: 16,802; {'Point': 8748, 'MultiPolygon': 8054}
With the entity filter, 16,802 amenity features remain: 8,748 points and 8,054 areas, and no lines.
Reproduce, check the duplicates, see which counts change
1. Reproduce the double count
fp = (osmium.FileProcessor(str(PBF)).with_areas()
.with_filter(osmium.filter.KeyFilter("amenity"))
.with_filter(osmium.filter.GeoInterfaceFilter(tags=["amenity"])))
raw = gpd.GeoDataFrame.from_features(fp, crs=4326)
lines = raw[raw.geom_type == "LineString"]
print(f"amenity features read: {len(raw):,}; {raw.geom_type.value_counts().to_dict()}")
print(f"lines that are closed rings: {lines.geometry.is_closed.sum():,} of {len(lines):,}")
amenity features read: 24,122; {'Point': 8748, 'MultiPolygon': 8054, 'LineString': 7320}
lines that are closed rings: 7,309 of 7,320
Without it, the same tag filter returns 24,122 features, including 7,320 lines. 7,309 of those lines are closed rings.
2. Check that the lines duplicate the polygons
Each closed line should have a polygon with the same outline. Compare their boundaries:
poly = raw[raw.geom_type == "MultiPolygon"]
closed = lines[lines.geometry.is_closed]
ring = gpd.GeoDataFrame(geometry=closed.geometry.apply(lambda g: Polygon(g.coords)), crs=4326)
match = gpd.sjoin(ring.to_crs(32618), poly.to_crs(32618)[["geometry"]], predicate="covered_by", how="left")
print(f"closed lines with an identical-outline polygon covering them: {match.index_right.notna().groupby(level=0).any().sum():,} of {len(closed):,}")
closed lines with an identical-outline polygon covering them: 7,309 of 7,309
Every one of the 7,309 closed lines lies on a polygon of the same outline: they are copies, and reading them inflates the count by 44%.
3. Which counts change
Count the five most common amenity types both ways:
both = pd.DataFrame({"read with duplicates": raw.amenity.value_counts(), "nodes and areas only": fixed.amenity.value_counts()})
both = both.sort_values("read with duplicates", ascending=False).head(5)
print(both.to_string())
read with duplicates nodes and areas only
amenity
parking 11396 6094
bench 2112 2093
restaurant 1160 1087
parking_space 1004 503
place_of_worship 913 585
Parking nearly doubles, from 6,094 to 11,396, because car parks are mostly areas. Parking spaces and places of worship are similarly inflated, while benches, almost all nodes, barely change.
The open lines that remain
The data used on this page
The Geofabrik OpenStreetMap extract for the District of Columbia dated 1 January 2026 (Β© OpenStreetMap contributors, ODbL), read with pyosmium 4.
from pathlib import Path
import geopandas as gpd
import osmium
import pandas as pd
import requests
from shapely.geometry import Polygon
Path("data").mkdir(exist_ok=True)
PBF = Path("data/district-of-columbia-260101.osm.pbf")
if not PBF.exists():
r = requests.get("https://download.geofabrik.de/north-america/us/district-of-columbia-260101.osm.pbf", timeout=600)
r.raise_for_status()
PBF.write_bytes(r.content)
print(f"{PBF.name} ready")
district-of-columbia-260101.osm.pbf ready
The dated D.C. extract is ready.
Example 1: the open lines that remain
A few amenity ways are genuinely lines, such as a bench drawn as a line or a row of parking. The geometry-type filter would drop them; the entity filter does too. List them before deciding:
open_lines = lines[~lines.geometry.is_closed]
print(f"open amenity lines: {len(open_lines)}; types {open_lines.amenity.value_counts().head(5).to_dict()}")
open amenity lines: 11; types {'bench': 11}
The only genuine lines are 11 benches drawn as lines, which either fix drops. Keep them separately if benches matter.
A closed way is ambiguous by design
In OpenStreetMap a closed way is ambiguous by design: the same ring of nodes may be a line, such as a roundabout or a fence, or the outline of an area, such as a building, depending on its tags. pyosmium does not guess. With with_areas() enabled, its area assembler builds an area from every closed way whose tags suggest one, and from every multipolygon relation, while the way itself still passes through the processor as a way. The GeoInterfaceFilter turns both into features, so each building or amenity outline arrives twice, once as a LineString and once as a MultiPolygon.
The clean fix is to choose entity types before geometry is built. EntityFilter(NODE | AREA) keeps point features and assembled areas and drops ways, which loses only the few amenities that really are open lines. Filtering on geometry type afterwards works too, but costs the time of building the lines first. Whichever is used, count before and after: a feature count that drops by a third, as here, is worth stating, because every analysis downstream inherits it.
Lines you need, area ids and members
- Lines you need: for highways, waterways and railways, keep
WAYand do not usewith_areas(); the doubling is a polygon-feature problem. - Area IDs: pyosmium gives areas their own IDs, twice the way ID for ways and twice plus one for relations, so IDs do not collide but do not match the way either.
- Multipolygon members: old-style multipolygons with tags on the outer way can still produce both a way feature and an area; the entity filter handles these too.
- Other readers: OSMnx, pyrosm and GDAL's OSM driver each decide differently; check geometry types whatever the tool.
Internal links
- How to read an OpenStreetMap PBF file in Python β the reader
- How to get points of interest from OpenStreetMap in Python β where the doubling bites
- Fixing OSM lakes that show as lines β the opposite problem
FAQ
Why does pyosmium return each building twice?
With with_areas(), a closed way is returned as a way (a LineString) and as an assembled area (a MultiPolygon).
How do I stop OSM features being counted twice?
Add osmium.filter.EntityFilter(osmium.osm.NODE | osmium.osm.AREA) before the tag filter, or drop LineString rows after reading.
Will dropping LineStrings lose data?
Only genuinely linear features of that tag; list the open lines first to check.
Does this happen with highways?
Not if you read highways as ways without with_areas(); it affects features that are areas.