Tessellation Fails or Produces Odd Cells

On this page

Problem statement

momepy.morphological_tessellation returns fewer cells than there are buildings, or cells that are slivers, or cells that overlap, or it runs for an hour. The footprints look fine on a map. The problem is almost always in the footprints anyway: overlapping polygons, duplicates, invalid rings, multipolygons, building parts stacked on top of their parent building, or tiny fragments that each demand their own cell.

A tessellation assumes a clean partition of the ground into non-overlapping footprints. Most real footprint layers β€” OSM above all β€” are not one until they are cleaned.

Quick answer

Check the footprints before tessellating, fix the five usual problems, and verify the result:

import numpy as np, geopandas as gpd, osmnx as ox, momepy
from shapely.geometry import box
from shapely.validation import make_valid

ox.settings.http_user_agent = "spatialworkflow-examples/1.0"
raw = ox.features_from_bbox((4.300, 52.072, 4.320, 52.084), {"building": True, "building:part": True})
raw = raw[raw.geom_type.isin(["Polygon", "MultiPolygon"])].to_crs(28992).reset_index()

def report(g, label):
    inter = gpd.sjoin(g[["geometry"]], g[["geometry"]], predicate="overlaps")
    print(f"{label:<14} {len(g):>5} rows | invalid {(~g.is_valid).sum():>3} | "
          f"multipart {(g.geom_type == 'MultiPolygon').sum():>3} | "
          f"overlapping pairs {len(inter) // 2:>4} | "
          f"duplicates {g.geometry.to_wkb().duplicated().sum():>3} | "
          f"< 10 mΒ² {(g.area < 10).sum():>3}")
report(raw, "as downloaded")
as downloaded   4486 rows | invalid   0 | multipart   0 | overlapping pairs   32 | duplicates   0 | < 10 mΒ² 486

Each of those counts is a way for the tessellation to go wrong. Building parts are the biggest source of overlaps in OSM: a tower mapped as a building:part on top of its building outline.

Drop parts, repair, explode, deduplicate

1. Drop building parts

building:part polygons describe sections of a building for 3D rendering. They overlap their parent. For tessellation, keep the outlines only.

2. Repair invalid geometries

make_valid fixes self-intersections and bad rings. Keep only the polygonal part of what it returns.

3. Explode or dissolve multipolygons consistently

A multipolygon building gets one cell per row. Either explode it into parts (more cells) or treat it as one building (one cell around both parts) β€” consistently.

4. Remove duplicates and overlaps

Identical geometries and buildings mapped twice by different sources produce zero-width cells. Drop exact duplicates; resolve overlaps by keeping the larger polygon or clipping one by the other.

5. Drop fragments

Sheds, kiosks and digitising slivers under a few mΒ² each get a cell that fragments the land around real buildings.

6. Verify

momepy.verify_tessellation reports buildings that did not get a cell and cells that came out as multipolygons.

Footprint problems that break a tessellation, with the symptom and fix for each. clean the footprints firstfootprint problemfixbuilding:part on its buildingdrop parts; keep outlinesinvalid ringsmake_valid, keep polygonsmultipolygonsexplode or dissolve, consistentlyduplicates and overlapsdrop duplicates; clip the smallersheds and sliversminimum area, e.g. 10 mΒ²Then check with momepy.verify_tessellation spatialworkflow.io
Five footprint problems, five symptoms in the tessellation.

Cleaning OSM buildings before tessellating

The data used on this page

OpenStreetMap buildings and building parts in a box over central The Hague, 4.300–4.320Β° E, 52.072–52.084Β° N, fetched by the quick answer's own code with OSMnx. The setup only points OSMnx at an Overpass mirror. OpenStreetMap is edited daily, so the setup reads its answers as saved on 10 October 2026.

import io
import os
import osmnx as ox
import requests
import zipfile

# OpenStreetMap as Overpass returned it on 10 October 2026, saved as osmnx's own cache so the counts
# below do not drift as OSM is edited; delete the cache folder to query live instead
if not os.path.exists("cache"):
    reply = requests.get("https://data.spatialworkflow.io/osm-cache-tessellation-fails-on-buildings-2026-10-10.zip", timeout=300)
    reply.raise_for_status()
    with zipfile.ZipFile(io.BytesIO(reply.content)) as archive:
        archive.extractall("cache")

ox.settings.overpass_url = "https://overpass.kumi.systems/api"
ox.settings.overpass_rate_limit = False          # this mirror has no /status endpoint to poll
ox.settings.requests_timeout = 600
print("Overpass:", ox.settings.overpass_url)
Overpass: https://overpass.kumi.systems/api

Example 1 β€” clean, then tessellate

b = raw[raw["building"].notna()].copy()                        # drop building:part-only rows
b["geometry"] = b.geometry.apply(make_valid)
b = b.explode(index_parts=False)
b = b[b.geom_type == "Polygon"]
b = b[~b.geometry.to_wkb().duplicated()]
b = b[b.area >= 10].reset_index(drop=True)
# resolve remaining overlaps: clip each smaller polygon by the larger ones it overlaps
b = b.iloc[np.argsort(-b.area.to_numpy())].reset_index(drop=True)      # largest first
for _ in range(3):                                             # clipping can expose new overlaps
    idx = gpd.sjoin(b[["geometry"]], b[["geometry"]], predicate="overlaps")
    if idx.empty:
        break
    for i, j in zip(idx.index, idx.index_right):
        if i > j:                                              # i is the smaller one
            b.at[i, "geometry"] = b.at[i, "geometry"].difference(b.at[j, "geometry"])
    b = b.explode(index_parts=False)
    b = b[(b.geom_type == "Polygon") & (b.area >= 10)].reset_index(drop=True)
report(b, "cleaned")
cells = momepy.morphological_tessellation(b, clip=momepy.buffered_limit(b, buffer=100))
print(f"cells: {len(cells)} for {len(b)} buildings")
cleaned         3932 rows | invalid   0 | multipart   0 | overlapping pairs    1 | duplicates   0 | < 10 mΒ²   0
cells: 3932 for 3932 buildings

Cleaning removes 553 rows β€” building parts, sheds, slivers β€” and 31 of the 32 overlapping pairs; one pair survives three rounds of clipping. There is now exactly one cell per building.

Example 2 β€” what the raw layer does to the tessellation

raw_poly = raw[raw.geom_type == "Polygon"].reset_index(drop=True)
raw_poly = raw_poly[raw_poly.is_valid]
t_raw = momepy.morphological_tessellation(raw_poly, clip=momepy.buffered_limit(raw_poly, buffer=100))
tiny = (t_raw.area < 1).sum()
print(f"raw polygons: {len(raw_poly)}, cells: {len(t_raw)}, cells under 1 mΒ²: {tiny}")
print(f"median cell: raw {t_raw.area.median():.0f} mΒ², cleaned {cells.area.median():.0f} mΒ²")
raw polygons: 4486, cells: 4485, cells under 1 mΒ²: 0
median cell: raw 178 mΒ², cleaned 204 mΒ²

The raw layer tessellates without an error and without any sub-square-metre slivers, which is what makes it dangerous. The 486 sheds and fragments under 10 mΒ² and the stacked building parts each claim a cell of their own, and the median cell shrinks from 204 to 178 mΒ². Nothing fails; every per-cell ratio computed afterwards is simply wrong for the buildings around them.

Example 3 β€” verify

excluded, multipart = momepy.verify_tessellation(cells, b)
print(f"buildings without a cell: {len(excluded)}; multipart cells: {len(multipart)}")
limit = momepy.buffered_limit(b, buffer=100)
print(f"limit area minus total cell area: {limit.area - cells.area.sum():,.1f} mΒ²")
print("flagged:", [(i, round(b.area[i], 1), b.geom_type[i]) for i in list(excluded) + list(multipart)])
buildings without a cell: 0; multipart cells: 1
limit area minus total cell area: -7,825.7 mΒ²
flagged: [(1157, np.float64(171.3), 'Polygon')]

The checks are not all green, and that is why they are run. Every building has a cell, but one cell came out in two pieces, and the cells add up to 7,826 mΒ² more than the limit β€” somewhere, cells overlap. The likely source is the one pair of footprints that still overlaps, next to the building with the two-piece cell. Fix or drop that building by hand, tessellate again, and verify again; repeat after every change to the footprints.

A building outline with a building part stacked on top, and the sliver cells that result when both are tessellated. two footprints fighting for the same landbuilding + parttower mapped as building:partresulting cellsslivers where both claim the landKeep outlines only: parts are for 3D rendering, not for plots spatialworkflow.io
A building part on top of its building: two footprints fighting for the same land.

Why overlaps break tessellation

Why overlaps break tessellation

Tessellation assigns each point of land to the nearest footprint. Where two footprints overlap, the land inside both is equidistant from both; the Voronoi step splits it arbitrarily, and the per-building dissolve can leave slivers or empty cells.

Why OSM needs the most cleaning

OSM mixes 2D outlines with 3D building parts, has duplicates where imports met hand mapping, and allows multipolygons with inner rings and detached outers. Cadastral and national datasets are usually already partitions; Overture removes many duplicates in conflation.

Why small fragments matter

Every footprint, however small, gets a cell. A garden shed gets a slice of its garden; a digitising sliver gets a slice of whatever surrounds it. Minimum-area filters remove noise that would otherwise be counted as buildings with extreme coverage.

Footprint problems in OSM buildings for central Den Haag as downloaded and after cleaning, with the tessellation checks. OSM footprints, central Den Haagas downloadedcleanedrows4,4863,932overlapping pairs321under 10 mΒ²4860median cell178 mΒ²204 mΒ²buildings without a cellβ€”0multipart cellsβ€”1The raw layer tessellates without an error; only the checks show what is wrong spatialworkflow.io
Cleaning fixes most of it; verification finds the rest.

Courtyards, touching buildings and ids

  • Courtyard buildings (polygons with holes) are fine; the hole becomes part of the cell.
  • Touching buildings are fine; only overlaps cause trouble.
  • Performance: tens of thousands of buildings are fine; hundreds of thousands need tiling by enclosures.
  • segment and shrink parameters assume metres; tessellating in degrees produces nonsense silently.
  • Keep an ID from the original data through cleaning so cells can be joined back.

FAQ

Why does my tessellation have fewer cells than buildings?

Duplicates, invalid geometries or overlaps. Clean the footprints and check with momepy.verify_tessellation.

Why are some cells tiny slivers?

Overlapping footprints β€” often OSM building parts on top of their buildings β€” split land between them.

Should I include building:part polygons?

No, not for tessellation. Use the building outlines.

How do I handle multipolygon buildings?

Explode them into parts or keep them as one building, consistently.

What minimum building size should I use?

Around 10 mΒ² removes sheds and slivers in most cities; adjust for the study.

Does the CRS matter?

Yes. Tessellation parameters are in the data's units; use metres.