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.

Wrong-looking blocks with the network feature that causes each and the fix. four network features behind wrong blockswhat you seecause โ†’ fixlong thin blocks along roadsdual carriageway โ†’ elongationfiltertiny triangles at junctionsmulti-node junction โ†’ consolidatehuge merged blocksstreet stops short โ†’ extend_lineszero-area facesdirected graph โ†’ to_undirectedIn Den Haag: 152 fragments and 111 slivers among 815 faces spatialworkflow.io
Four network features behind almost every wrong block.

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.

Faces from polygonising the Den Haag drive network by kind: plausible blocks, carriageway slivers, junction fragments and oversized faces. 815 faces from 4 kmยฒ of Den Haagplausible block546 ยท median 7,058 mยฒfragment (< 200 mยฒ)152 ยท median 82 mยฒcarriageway sliver111 ยท median 970 mยฒoversized (> 10 ha)6 ยท median 13 ha0300600A third of the faces are street space, not blocks spatialworkflow.io
What polygonising a raw street network actually produces.

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.

Faces, fragments and slivers from the raw Den Haag network, after consolidating junctions and after extending dangling lines. what each network fix changed herenetworkfacesunder 200 mยฒsliversraw815152122consolidated, 12 m764120147extended, 15 m815152122filtered552 blocks00Median block 2,371 mยฒ before filtering, 7,194 mยฒ after spatialworkflow.io
Consolidation trades fragments for slivers; extension changed nothing here; filtering does the work.

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.

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.