Blocks from Streets Come Out Wrong
On this page
Problem statement
Polygonising the street network was supposed to give the city's blocks. Instead there are hundreds of thin slivers along the main roads, tiny triangles at every large junction, a few enormous "blocks" that swallow several real ones, and a count that bears no relation to what a map shows. The polygonisation is doing exactly what it should; the street network is not the clean set of block boundaries it looks like.
Four features of real street data cause almost all of it: dual carriageways mapped as two lines, junctions mapped as several nodes, lines that end without meeting another line, and a boundary set that includes or omits the wrong kinds of way.
Quick answer
Diagnose by shape and size, then fix the network or filter the faces:
import numpy as np, geopandas as gpd, osmnx as ox, momepy
from shapely.geometry import box
ox.settings.http_user_agent = "spatialworkflow-examples/1.0"
ox.settings.overpass_url = "https://maps.mail.ru/osm/tools/overpass/api" # overpass-api.de mirror
ox.settings.overpass_rate_limit = False
G = ox.graph_from_point((52.0799, 4.3113), dist=1500, network_type="drive")
streets = ox.graph_to_gdfs(ox.convert.to_undirected(G), nodes=False).to_crs(28992)
aoi = gpd.GeoSeries([box(80000, 454000, 82000, 456000)], crs=28992)
faces = momepy.enclosures(streets, limit=aoi)
faces["area"] = faces.area
faces["elong"] = momepy.elongation(faces)
kind = np.select([faces.area < 200, (faces.elong < 0.15) & (faces.area < 5000),
faces.area > 100_000],
["junction fragment", "carriageway sliver", "oversized"], "plausible block")
print(faces.assign(kind=kind).groupby("kind").agg(count=("area", "size"),
median_m2=("area", "median")).round(0).to_string())
count median_m2
kind
carriageway sliver 111 970.0
junction fragment 152 82.0
oversized 6 132928.0
plausible block 546 7058.0
Fragments and slivers are street space counted as blocks; oversized faces are usually several blocks merged because a street that should divide them is missing from the network or not connected.
Undirected network, slivers, junction fragments, open ends
1. Use an undirected network
Directed graphs duplicate two-way streets; polygonising two identical lines produces zero-area faces.
2. Find the slivers
Long, thin faces between parallel lines a few metres apart are dual carriageways. Filter them by elongation and area, or collapse the two carriageways to one centreline before polygonising.
3. Find the junction fragments
Complex junctions mapped as several nodes create small triangles and islands. Consolidate nearby intersections, or drop faces below a minimum area.
4. Close the open ends
A street that stops a metre short of the next does not split the block. Extend dangling lines to the nearest line within a tolerance (momepy.extend_lines).
5. Check the boundary set
Service roads, footpaths, railways and rivers change which faces exist. Decide which ways bound a block and apply the choice consistently.
Consolidating junctions and extending dangling ends
Example 1 โ consolidate junctions before polygonising
Gc = ox.consolidate_intersections(ox.project_graph(G), tolerance=12,
rebuild_graph=True, dead_ends=False)
streets_c = ox.graph_to_gdfs(ox.convert.to_undirected(Gc), nodes=False).to_crs(28992)
faces_c = momepy.enclosures(streets_c, limit=aoi)
for label, f in (("raw network", faces), ("consolidated 12 m", faces_c)):
print(f"{label:<18} {len(f):>4} faces, {(f.area < 200).sum():>3} under 200 mยฒ, "
f"{((momepy.elongation(f) < 0.15) & (f.area < 5000)).sum():>3} slivers")
raw network 815 faces, 152 under 200 mยฒ, 122 slivers
consolidated 12 m 764 faces, 120 under 200 mยฒ, 147 slivers
Consolidation merges clustered junction nodes and removes a fifth of the small fragments (152 โ 120). It is not free: the sliver count rises from 122 to 147, because moving junction nodes to their cluster centre bends the lines between them, and some fragments become long thin faces instead. Slivers between carriageways further apart than the tolerance are not touched. Consolidate, then still filter.
Example 2 โ extend dangling ends
extended = momepy.extend_lines(streets, tolerance=15, target=streets)
faces_e = momepy.enclosures(extended, limit=aoi)
print(f"faces before extending: {len(faces)}, after: {len(faces_e)}; "
f"oversized before {(faces.area > 100_000).sum()}, after {(faces_e.area > 100_000).sum()}")
faces before extending: 815, after: 815; oversized before 6, after 6
Here extending changes nothing: 815 faces and six oversized ones, before and after. The Den Haag drive network has no dangling ends within 15 m of another street, so its six oversized faces are large for another reason โ a park, or a block divided only by ways that are not in the drive network. The step is still worth running; on hand-traced networks with gaps at junctions it is what closes blocks.
Example 3 โ filter what remains, and keep the evidence
clean = faces[(faces.area >= 200) & ~((faces.elong < 0.15) & (faces.area < 5000))].copy()
clean["flag_oversized"] = clean.area > 100_000
print(f"{len(faces)} faces โ {len(clean)} blocks; {clean.flag_oversized.sum()} flagged as oversized")
print(f"median block {clean.area.median():,.0f} mยฒ (all faces: {faces.area.median():,.0f} mยฒ)")
815 faces โ 552 blocks; 6 flagged as oversized
median block 7,194 mยฒ (all faces: 2,371 mยฒ)
Filtering triples the median block size โ the slivers and fragments were dragging it down. Flagging rather than deleting oversized faces keeps parks and genuinely large blocks while marking them for a check.
Dual carriageways, junction clusters and missing lines
Why OSM maps dual carriageways as two lines
OSM represents each direction of a physically separated road as its own way, because they are separate for routing. That is correct for navigation and wrong for block extraction, where the whole road is one boundary.
Why junctions become clusters of nodes
Large junctions have turn lanes, slip roads and islands, each mapped as its own way meeting at its own node. The planar graph therefore has many small faces inside every big junction.
Why oversized faces mean missing lines
A face is bounded by lines; if a street is missing, not connected, or excluded by the network type (a service road, a footpath), the faces on either side merge. Oversized faces are the best pointer to where the network is incomplete for the question.
Tolerances, roundabouts and motorways
- Tolerance choices (consolidation, extension) are parameters; report them.
- Roundabout centres are small round faces; filter them with the fragments.
- Bridges cross streets without junctions; polygonising 2D lines splits faces at the crossing.
- Motorways bound very large faces; exclude them from the network if they are not block boundaries in your sense.
- Validation: overlay the result on imagery for a sample before trusting counts.
Internal links
- How to extract urban blocks from a street network โ the workflow on a clean network
- Urban blocks explained โ what a block is
- How to calculate street network metrics โ consolidating intersections for counts
- Street networks as graphs explained โ how OSM streets become a graph
- Tessellation fails or produces odd cells โ the building-side equivalent
- Snap and align geometries: slivers and gaps โ slivers in general
FAQ
Why do I get thin sliver blocks along main roads?
Dual carriageways are mapped as two lines; the space between them becomes a face.
Why are there tiny polygons at junctions?
Complex junctions are mapped as clusters of nodes and short ways. Consolidate intersections or filter by area.
Why is one block enormous?
A street that should split it is missing, unconnected or excluded from the network.
How do I close gaps between lines?
Extend dangling ends to the nearest line within a tolerance, for example with momepy.extend_lines.
Should I delete oversized blocks?
Flag them; some are real (parks, campuses) and some reveal missing streets.
Which streets should bound blocks?
It depends on the question โ traffic blocks or pedestrian blocks โ and must be consistent.