Morphological Tessellation Explained
On this page
Problem statement
Density, coverage and many other form measures are ratios of a building to "its land". In most countries the land a building stands on is recorded as a cadastral parcel — but parcels are legal objects. One parcel may hold a whole housing block owned by one company; one building may straddle several parcels; streets and parks are parcels of their own, sometimes enormous. And in much of the world there is no open parcel data at all.
Morphological tessellation offers a geometric substitute: every square metre of land is given to the building whose footprint is nearest. The result is consistent everywhere, available wherever footprints are, and different from parcels in ways worth understanding before using it as their proxy.
Quick answer
A tessellation cell is the set of points closer to one building's footprint than to any other, cut to a limit:
- it partitions the land: no gaps, no overlaps, one cell per building;
- it follows footprint boundaries, not centroids, so big buildings get proportionally more land;
- it ignores ownership and streets unless bounded by street enclosures.
import requests, numpy as np, geopandas as gpd, momepy
from shapely.geometry import box
cx, cy = 81000, 455000
aoi = box(cx - 500, cy - 500, cx + 500, cy + 500)
b = gpd.read_parquet("denhaag_bag.parquet")
b = b[b.representative_point().within(aoi) & b.is_valid & (b.area > 10)].reset_index(drop=True)
cells = momepy.morphological_tessellation(b, clip=momepy.buffered_limit(b, buffer=100))
feats, start = [], 0
while True:
fc = requests.get("https://service.pdok.nl/kadaster/kadastralekaart/wfs/v5_0", params={
"service": "WFS", "version": "2.0.0", "request": "GetFeature",
"typeNames": "kadastralekaart:Perceel", "count": 1000, "startIndex": start,
"outputFormat": "application/json", "bbox": f"{cx-500},{cy-500},{cx+500},{cy+500},EPSG:28992"},
timeout=300).json()
feats += fc["features"]
if len(fc["features"]) < 1000:
break
start += 1000
parcels = gpd.GeoDataFrame.from_features(feats, crs=28992)
parcels = parcels[parcels.representative_point().within(aoi)]
print(f"1 km² of central Den Haag: {len(b):,} buildings, {len(cells):,} cells, "
f"{len(parcels):,} cadastral parcels")
print(f"median cell {cells.area.median():.0f} m², median parcel {parcels.area.median():.0f} m²")
1 km² of central Den Haag: 1,990 buildings, 1,990 cells, 2,367 cadastral parcels
median cell 223 m², median parcel 125 m²
Cells and parcels are the same order of magnitude — the median cell is not quite twice the median parcel — and not the same objects: one follows buildings, the other follows ownership.
What a cell represents, a limit, street bounds
1. Know what a cell represents
A cell is "the land nearest this building". In a terrace that approximates the house plot plus half the street in front; around a detached house, its garden and half the space to the neighbours.
2. Decide on a limit
Without one, edge cells extend indefinitely. A buffered limit around the buildings (commonly 100 m) keeps cells from swallowing fields and water.
3. Decide whether streets bound the cells
A free tessellation shares each street between the buildings on both sides. An enclosed tessellation stops at the street — closer to parcels and to what a plot means.
4. Compare with parcels where you can
Where cadastral parcels exist, check how cell areas relate to parcel areas for your area. The relationship tells you how far to trust cell-based density measures elsewhere.
Cells against parcels for the same buildings
The data used on this page
Building footprints with LiDAR heights from the 3D BAG (the Dutch building register with heights from AHN), fetched from its WFS for central Den Haag — 4.285–4.330° E, 52.065–52.095° N — exactly as in How to download building footprints from OSM and Overture, and saved as denhaag_bag.parquet in the Dutch national grid (EPSG:28992).
import os
import geopandas as gpd
import requests
from pyproj import Transformer
if not os.path.exists("denhaag_bag.parquet"):
W, S, E, N = 4.285, 52.065, 4.330, 52.095
to_rd = Transformer.from_crs(4326, 28992, always_xy=True)
(x0, y0), (x1, y1) = to_rd.transform(W, S), to_rd.transform(E, N)
feats, start = [], 0
while True:
fc = requests.get("https://data.3dbag.nl/api/BAG3D/wfs", params={
"service": "WFS", "version": "2.0.0", "request": "GetFeature", "typeNames": "BAG3D:lod12",
"count": 5000, "startIndex": start, "outputFormat": "application/json",
"bbox": f"{x0},{y0},{x1},{y1},EPSG:28992"}, timeout=300).json()
feats += fc["features"]
if len(fc["features"]) < 5000:
break
start += 5000
bag = gpd.GeoDataFrame.from_features(feats, crs=28992)
bag["height"] = bag.b3_h_70p - bag.b3_h_maaiveld
bag[["identificatie", "b3_h_70p", "b3_h_maaiveld", "height", "b3_opp_grond", "geometry"]] \
.to_parquet("denhaag_bag.parquet")
print(len(gpd.read_parquet("denhaag_bag.parquet")), "buildings")
23929 buildings
Example 1 — cell against parcel for the same building
pts = gpd.GeoDataFrame(geometry=b.representative_point(), crs=28992)
j = gpd.sjoin(pts, parcels[["geometry"]], predicate="within")
j = j[~j.index.duplicated()]
ratio = cells.area.reindex(j.index).to_numpy() / parcels.area.loc[j.index_right].to_numpy()
print(f"buildings matched to a parcel: {len(j):,} of {len(b):,}")
print(f"cell area / parcel area: median {np.median(ratio):.2f}, "
f"10th–90th percentile {np.percentile(ratio, 10):.2f}–{np.percentile(ratio, 90):.2f}")
per_parcel = j.groupby("index_right").size()
print(f"parcels holding several buildings: {(per_parcel > 1).mean():.0%} "
f"(most: {per_parcel.max()} buildings)")
buildings matched to a parcel: 1,960 of 1,990
cell area / parcel area: median 1.34, 10th–90th percentile 0.20–4.04
parcels holding several buildings: 11% (most: 25 buildings)
The typical cell is a third larger than the parcel its building stands on, mostly because it takes in half the street. The spread — from a fifth of the parcel to four times it — is the honest answer to "is a cell a plot?": sometimes close, sometimes far, most often where one owner holds many buildings or one building spans several parcels.
Example 2 — the limit distance
for d in (50, 100, 200):
t = momepy.morphological_tessellation(b, clip=momepy.buffered_limit(b, buffer=d))
cov = b.area.sum() / t.area.sum()
print(f"limit {d:>3} m: total cell area {t.area.sum() / 1e6:.3f} km², "
f"coverage {cov:.0%}, median cell {t.area.median():.0f} m²")
limit 50 m: total cell area 1.189 km², coverage 40%, median cell 223 m²
limit 100 m: total cell area 1.450 km², coverage 33%, median cell 223 m²
limit 200 m: total cell area 1.956 km², coverage 24%, median cell 223 m²
The median cell does not move at all: in a dense centre the limit leaves the interior cells alone. The edge cells absorb the difference, and there are enough of them in a 1 km square that total cell area grows by two-thirds and overall coverage falls from 40% to 24% between a 50 m and a 200 m limit. In a suburb with large gardens and open land, the interior changes too. The limit is part of the method.
Example 3 — where the street land goes
public = parcels[parcels.area > 5000]
print(f"parcels over 5,000 m²: {len(public)} covering {public.area.sum() / 1e6:.3f} km² "
f"of the 1 km² (streets, squares, water, large estates)")
print(f"land within the buffered limit not under any building: "
f"{(cells.area.sum() - b.area.sum()) / 1e6:.3f} km², all of it assigned to some building's cell")
parcels over 5,000 m²: 20 covering 0.234 km² of the 1 km² (streets, squares, water, large estates)
land within the buffered limit not under any building: 0.971 km², all of it assigned to some building's cell
In the cadastre, streets and squares are parcels of their own. In a free tessellation that land is shared out among the buildings along it — which raises cell areas and lowers cell coverage wherever streets are wide.
Footprints rather than centroids
Why footprints rather than centroids
A Voronoi diagram of centroids draws cell boundaries halfway between building centres, which gives a long building the same share of land on its ends as a small one. Sampling points along each footprint's boundary makes the boundary halfway between walls, which is how land around buildings is actually used.
Why tessellation is used even where parcels exist
Parcels are recorded for legal and fiscal reasons. A single parcel can hold a hundred flats; a row of houses can share a parcel with a garage court. Measures built on parcels mix form with ownership history. Tessellation measures form only — and it is identical in method in every city, which parcels never are.
Why enclosed tessellation is the usual compromise
Free cells give street land to buildings; parcels give it to the municipality. Tessellating within street enclosures excludes the street, keeps the geometric consistency and gives cells that look like plots.
Overlaps, tiny buildings and AOI edges
- Overlapping footprints break the partition; fix them before tessellating.
- Very small buildings (sheds) get small cells that fragment gardens; filter by a minimum area if they are not of interest.
- Buildings on the AOI edge have truncated cells; exclude them from statistics.
- Water and railways should be barriers in an enclosed tessellation.
- Cadastral data is open in the Netherlands, Denmark and elsewhere; licensing varies by country.
Internal links
- How to create a morphological tessellation — the workflow
- Tessellation fails or produces odd cells — the fix guide
- How to calculate coverage and floor area ratio — measures on cells
- Urban blocks explained — the street-bounded unit above the cell
- How to build Voronoi service areas — Voronoi diagrams in another setting
- Urban morphology explained — cells among the elements of form
FAQ
What is a morphological tessellation?
A partition of land into cells, one per building, each containing the points closer to that building's footprint than to any other.
Is a tessellation cell the same as a plot?
No. It approximates one, most closely when bounded by streets. Cadastral parcels follow ownership.
Why not use a Voronoi diagram of building centroids?
Centroid Voronoi cells ignore building size and shape; footprint-based tessellation does not.
What is the limit for?
It stops edge cells from extending into open land; a 100 m buffer around buildings is common in cities.
Where can I get cadastral parcels?
From national cadastres where they are open — in the Netherlands through PDOK's Kadastrale Kaart WFS.
Why do cells and parcels differ so much in places?
Parcels group or split buildings by ownership, and streets are separate parcels; cells do neither.