Building Density Measures Explained

On this page

Problem statement

"Dense" is used for the medieval core of a European city, a Hong Kong housing estate and a Manhattan office district, and each is dense in a different way. The first covers most of its ground with low buildings; the second stacks tall towers on a small share of its ground; the third does both. A single density figure β€” homes per hectare, people per kmΒ², floor area ratio β€” cannot tell them apart.

Urban designers settled this decades ago with a small set of linked measures, known after the Dutch work that popularised them as Spacemate: how much ground is covered, how much floor is built, and the two ratios that follow. Together they place any area on a map of form, not just of quantity.

Quick answer

GSI  = building footprint / land area          ground coverage, 0–1
FSI  = gross floor area / land area             floor area ratio, 0 to 10+
L    = FSI / GSI                                average storeys
OSR  = (1 βˆ’ GSI) / FSI                          open space per mΒ² of floor
import numpy as np, geopandas as gpd
from shapely.geometry import box

b = gpd.read_parquet("denhaag_bag.parquet")
b = b[b.is_valid & (b.area > 10)].reset_index(drop=True)
b["floors"] = np.maximum(1, np.round((b.b3_h_70p - b.b3_h_maaiveld).clip(lower=2.5) / 3.2))
b["gfa"] = b.area * b.floors

x0, y0, x1, y1 = b.total_bounds
grid = gpd.GeoDataFrame(geometry=[box(x, y, x + 250, y + 250)
                                  for x in np.arange(x0, x1 - 250, 250)
                                  for y in np.arange(y0, y1 - 250, 250)], crs=b.crs)
pts = gpd.GeoDataFrame(b[["gfa"]].assign(fp=b.area), geometry=b.representative_point(), crs=b.crs)
agg = gpd.sjoin(pts, grid, predicate="within").groupby("index_right")[["fp", "gfa"]].sum()
agg["GSI"] = agg.fp / 62_500
agg["FSI"] = agg.gfa / 62_500
agg["L"] = agg.FSI / agg.GSI
agg["OSR"] = (1 - agg.GSI) / agg.FSI
print(f"{len(agg)} squares of 250 m with buildings")
print(agg[["GSI", "FSI", "L", "OSR"]].describe(percentiles=[.1, .5, .9]).loc[
    ["10%", "50%", "90%"]].round(2).to_string())
174 squares of 250 m with buildings
      GSI   FSI     L   OSR
10%  0.08  0.34  3.03  0.23
50%  0.32  1.28  3.98  0.52
90%  0.47  2.50  7.08  2.65

Across central Den Haag's 250 m squares, coverage and floor area vary several-fold, and so does the average height β€” which is exactly the variation a single density number hides.

Coverage and floor area separately, then height and open space

1. Measure coverage and floor area separately

GSI needs footprints; FSI needs footprints and storeys. Keep both.

2. Derive height and open space

L (storeys) and OSR (open space per floor area) follow from the two. They are what make a GSI–FSI pair legible to a designer.

3. Fix the land unit

A plot, a block, a grid square or a district. Larger units include more streets and open space, so the same buildings give lower ratios. Compare like with like.

4. Plot GSI against FSI

On a GSI–FSI chart, lines through the origin are constant storeys; curves are constant open space. Types of urban fabric occupy recognisable regions.

5. Add population or dwellings only on top

People and homes per hectare depend on floor area per person, which varies by country and income. Form measures are the base; occupancy is a second layer.

The GSI–FSI plane with lines of constant storeys and example fabric types. coverage across, floor area up0.000.250.500.751.000123GSI (ground coverage)FSI1 storeys2 storeys4 storeys8 storeystowers in open spaceclosed blocksterraces spatialworkflow.io
Coverage across, floor area up: each fabric type has its region.

Same floor area, different forms

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 β€” same floor area, different forms

target = agg.FSI.median()
similar = agg[(agg.FSI - target).abs() < 0.15].sort_values("L")
lo, hi = similar.iloc[0], similar.iloc[-1]
print(f"two squares with FSI near {target:.2f}:")
for label, r in (("lowest-rise", lo), ("highest-rise", hi)):
    print(f"  {label:<13} GSI {r.GSI:.2f}, FSI {r.FSI:.2f}, L {r.L:.1f} storeys, OSR {r.OSR:.2f}")
two squares with FSI near 1.28:
  lowest-rise   GSI 0.45, FSI 1.26, L 2.8 storeys, OSR 0.44
  highest-rise  GSI 0.17, FSI 1.37, L 8.2 storeys, OSR 0.61

The same amount of floor space, arranged as low buildings covering much of the ground or taller ones covering less. FSI alone reports them as equal.

Example 2 β€” the unit changes the ratio

whole = {"GSI": b.area.sum() / ((x1 - x0) * (y1 - y0)),
         "FSI": b.gfa.sum() / ((x1 - x0) * (y1 - y0))}
print(f"whole extent:        GSI {whole['GSI']:.2f}, FSI {whole['FSI']:.2f}")
print(f"median 250 m square: GSI {agg.GSI.median():.2f}, FSI {agg.FSI.median():.2f}")
print(f"only squares > 30% covered: GSI {agg[agg.GSI > .3].GSI.median():.2f}, "
      f"FSI {agg[agg.GSI > .3].FSI.median():.2f}")
whole extent:        GSI 0.28, FSI 1.38
median 250 m square: GSI 0.32, FSI 1.28
only squares > 30% covered: GSI 0.40, FSI 1.61

Three honest-sounding summaries of the same buildings disagree, and not even in one direction. The median square is more covered than the whole extent (0.32 against 0.28), because the extent includes parks, water and wide streets; but its floor area ratio is lower (1.28 against 1.38), because the towers concentrate floor space in a minority of squares that a median ignores and a total does not. Keeping only the built-up squares raises both.

Example 3 β€” a rough fabric classification

def fabric(r):
    if r.GSI < 0.15:
        return "open (low coverage)"
    if r.L >= 5:
        return "high-rise"
    if r.GSI >= 0.40:
        return "compact low/mid-rise"
    return "mid-rise, mixed"
agg["type"] = agg.apply(fabric, axis=1)
print(agg.type.value_counts().to_string())
type
mid-rise, mixed         70
high-rise               37
compact low/mid-rise    36
open (low coverage)     31

Thresholds like these are conventions, not laws, but they turn two continuous ratios into categories a planner recognises β€” and make clear that "density" was always at least two numbers.

Two blocks with the same floor area ratio: low buildings covering most of the ground against towers in open space. equal floor area ratio, opposite formsGSI 0.6 Β· L 2FSI 1.2GSI 0.12 Β· L 10FSI 1.2Plan view: the floor area is identical, the city is not spatialworkflow.io
Equal floor area ratio, opposite forms.

Two measures are the minimum

Why two measures are the minimum

Any built volume has a footprint and a height; density needs both to be described. GSI captures the first, L the second, and FSI is their product. Reporting FSI alone throws away the split between them, which is precisely what distinguishes a terrace from a tower.

Why OSR is useful

OSR says how much unbuilt land there is per square metre of floor β€” how crowded the open space is likely to feel. Two areas with the same coverage can have very different OSR if one is much taller: the same courtyard shared by twice as many floors.

Why occupancy is a separate question

Floor area per person ranges from under 20 mΒ² to over 60 mΒ² between countries and incomes. People per hectare mixes form with how the floor is used. Built-form density is stable over decades; occupancy changes with every household.

Number of 250 m squares in central Den Haag in each fabric type defined from GSI and storeys. 174 squares of 250 m, by fabric typemid-rise, mixed70high-rise (L β‰₯ 5)37compact (GSI β‰₯ 0.40)36open (GSI < 0.15)3104080Thresholds are conventions; two ratios make categories a planner recognises spatialworkflow.io
Two ratios, four fabric types, one square kilometre after another.

Floor heights, non-residential floors and parks

  • Floor height assumption: FSI from LiDAR heights depends on an assumed floor-to-floor height; report it.
  • Non-residential floor counts in FSI; residential FSI needs use data.
  • Underground floors are invisible to heights.
  • Water and parks in the land unit lower every ratio; decide whether to use gross or net land.
  • Other names: FSI is FAR; GSI is building coverage ratio or site coverage.

FAQ

What is the difference between FAR and coverage?

Coverage (GSI) is the share of land under buildings; FAR (FSI) is floor area divided by land area, so it includes height.

What is Spacemate?

A framework using GSI, FSI, average storeys and open space ratio together to describe built density.

Can two areas have the same FAR and look different?

Yes β€” low buildings with high coverage and tall buildings with low coverage can share an FAR.

What is the open space ratio?

Unbuilt land per unit of floor area: (1 βˆ’ GSI) / FSI.

How do I get floor area?

Footprint area times number of storeys, from a register or estimated from height.

Why do density figures for the same city differ?

Different land units, source data and floor-height assumptions.