Development Capacity & Buildable Area Analysis

A rezone lands. The question that follows within the hour is always the same: how much can now be built there? Answering it turns zoning from a label into a number, and it is the step where a spatial pipeline stops describing regulation and starts producing an estimate somebody will price a deal on. That shift raises the standard sharply. A wrong zoning code is a data error; a wrong unit-yield figure is an investment memo. This topic covers computing buildable area and development capacity so that the number carries its own assumptions, as part of the Spatial Impact Analysis & Zoning Change Detection area. It consumes the effective standards produced by overlay district modeling, because capacity computed from a base code alone ignores whichever overlay actually caps the height.

Prerequisites and operational context jump to heading

Capacity analysis is the most assumption-dependent work in this whole area, and its prerequisites are correspondingly strict.

Geometry must be in a projected CRS with linear units. Every operation on this page is a distance or an area, and both are meaningless in degrees. A setback buffer of “25” applied to a layer in EPSG:4326 offsets by twenty-five degrees, which is most of a continent; the failure is so large it is usually caught, but the same mistake at a smaller scale — computing area in Web Mercator, which inflates it by about 70% at 40° north — produces a plausible number that is simply wrong.

Effective standards must be resolved, not looked up from the base code. Setback, height, floor-area ratio, and lot coverage each come from whichever rule wins after overlays compose. Reading them off the base district is the single most common source of capacity errors, and it fails in the dangerous direction: base standards are usually more permissive than the overlay that modifies them, so the resulting estimate is too high.

Every parcel needs a frontage determination. Front, side, and rear setbacks are different numbers, and applying them requires knowing which edge is the front. That is not in the data. It is inferred from adjacency to a street centreline, and on corner lots, flag lots, and through lots it is genuinely ambiguous. A capacity pipeline that applies a uniform buffer instead has assumed the front setback applies on all four sides, which understates capacity on most lots and overstates it on none — a bias worth knowing about rather than discovering.

Architecture: subtract constraints, then measure what survives jump to heading

Buildable area is a subtraction problem, and its reliability comes from doing the subtractions in a fixed order with each intermediate geometry retained. The intermediates are what make a surprising number explicable.

Buildable envelope as an ordered series of subtractions A plan view of one parcel shown four times, left to right. The first shows the full lot at 1200 square metres. The second shows the same lot with front, side and rear setbacks removed, leaving 610 square metres. The third removes an easement strip crossing the rear of the lot and a small wetland buffer at one corner, leaving 505 square metres. The fourth shows the maximum footprint after the lot coverage limit of 40 per cent is applied, which caps the footprint at 480 square metres — below the 505 square metres the geometry allows, so coverage rather than geometry is the binding constraint. A caption notes that the binding constraint differs per parcel and is the single most useful output of the analysis. 1 · the lot 1 200 m² as platted 2 · less setbacks 610 m² front 25 ft · side 8 ft · rear 20 ft 3 · less encumbrances 505 m² utility easement · wetland buffer 4 · less coverage cap 480 m² 40% of 1 200 m² — the binding limit Stage 4 is smaller than stage 3, so lot coverage binds here, not geometry. On a narrow lot the setbacks bind instead, and on a tall-zoned lot the height limit does. Naming the binding constraint per parcel is the most useful output of all.
Four subtractions, each retained. The last stage is smaller than the third, so on this lot coverage is the binding constraint rather than geometry — and knowing which constraint binds is what makes a capacity number actionable, because it says what a variance would have to address.

The output of this pipeline is therefore not a number but a small record: buildable area, the binding constraint, and the intermediate areas. A consumer who receives only “480 m²” cannot tell whether relaxing the coverage limit would help; a consumer who receives “480 m², bound by coverage, geometry allows 505” can.

Which standard binds, and why it varies jump to heading

Standard Binds when Typical lot type Effect of relaxing it
Front / side setbacks the lot is narrow relative to its depth urban infill, row lots large gain in footprint
Rear setback the lot is shallow corner and through lots moderate gain
Lot coverage the lot is large and the setbacks are modest suburban single-family proportional gain
Height the use is dense and the footprint is already capped mixed-use, transit corridors gain in floor area, not footprint
Floor-area ratio the site could otherwise build more storeys than FAR allows mid-rise districts gain in floor area
Easement or buffer an encumbrance crosses the developable middle rural, riparian, utility corridors often cannot be relaxed at all

The last row deserves emphasis because it behaves differently from the rest: a setback is a regulatory choice a variance can address, while a recorded easement is a property right that zoning relief does not touch. Reporting them in the same field, as though both were “constraints,” implies a flexibility that does not exist.

Production implementation jump to heading

The envelope computation below keeps every intermediate and names the binding constraint. It assumes a projected CRS in metres and effective standards already resolved through overlay composition.

from dataclasses import dataclass, field

from shapely.geometry import Polygon
from shapely.ops import unary_union

FT_TO_M = 0.3048


@dataclass
class Capacity:
    lot_area_m2: float
    after_setbacks_m2: float
    after_encumbrances_m2: float
    max_footprint_m2: float
    binding: str
    envelope: Polygon | None
    floor_area_m2: float | None = None
    unit_yield: int | None = None
    assumptions: list[str] = field(default_factory=list)


def buildable_envelope(parcel, std, frontage_edges, encumbrances) -> Capacity:
    """parcel.geometry is a projected Polygon; `std` is the RESOLVED standard set
    from overlay composition; `frontage_edges` names which edges are the front."""
    lot = parcel.geometry
    lot_area = lot.area
    notes = []

    # ---- setbacks: offset each edge by its own distance, not one uniform buffer
    if frontage_edges is None:
        # No frontage determination available. A uniform front-setback buffer is
        # the conservative choice, and it must be declared rather than hidden.
        inner = lot.buffer(-std.numeric["front_setback_ft"] * FT_TO_M,
                           join_style=2)
        notes.append("no frontage determination: front setback applied on all edges "
                     "(understates capacity)")
    else:
        inner = _offset_per_edge(lot, std, frontage_edges)

    if inner.is_empty:
        return Capacity(lot_area, 0.0, 0.0, 0.0, "setbacks", None, assumptions=notes)
    if inner.geom_type == "MultiPolygon":
        # Setbacks split the lot into disjoint pieces — common on flag lots. Only
        # the largest piece can hold a principal structure.
        pieces = sorted(inner.geoms, key=lambda g: g.area, reverse=True)
        notes.append(f"setbacks split the lot into {len(pieces)} pieces; "
                     f"largest retained ({pieces[0].area:.0f} m²)")
        inner = pieces[0]

    after_setbacks = inner.area

    # ---- encumbrances: easements, buffers, floodway. Property rights, not zoning.
    if encumbrances:
        blocked = unary_union([e.geometry for e in encumbrances])
        inner = inner.difference(blocked)
        if inner.geom_type == "MultiPolygon":
            inner = max(inner.geoms, key=lambda g: g.area)
        notes.append(f"{len(encumbrances)} encumbrance(s) subtracted; these are not "
                     f"subject to zoning relief")
    after_enc = inner.area

    # ---- coverage cap applies to the LOT, not to the setback envelope
    coverage_cap = lot_area * std.numeric.get("lot_coverage", 1.0)

    if coverage_cap < after_enc:
        footprint, binding = coverage_cap, "lot_coverage"
    elif after_enc < after_setbacks - 1e-9:
        footprint, binding = after_enc, "encumbrance"
    else:
        footprint, binding = after_enc, "setbacks"

    cap = Capacity(lot_area, after_setbacks, after_enc, footprint, binding, inner,
                   assumptions=notes)

    # ---- floor area: the lesser of what height allows and what FAR allows
    storeys = int(std.numeric["height_ft"] // std.numeric.get("storey_height_ft", 11.0))
    by_height = footprint * storeys
    by_far = lot_area * std.numeric.get("far", float("inf"))
    cap.floor_area_m2 = min(by_height, by_far)
    if by_far < by_height:
        cap.binding = "far"
        cap.assumptions.append(f"FAR binds before height ({storeys} storeys would fit)")
    return cap

Note that the coverage cap is computed against the lot area, not against the setback envelope. This is a real ordinance detail that is easy to get wrong in code, and getting it wrong inflates capacity on every parcel with generous setbacks.

The assumptions list is not decoration. Every capacity figure that leaves this function carries the list, and a consumer that discards it is consuming a number whose meaning it does not know — the missing-frontage case in particular produces a systematically low estimate that looks identical to a carefully computed one.

Edge cases and gotchas jump to heading

Negative buffers that invert. buffer(-d) on a lot narrower than 2d returns an empty geometry, which is correct, but on a concave lot it can return a shape with unexpected topology before emptying. Always check is_empty and geom_type after an inward offset rather than assuming a polygon comes back.

Flag lots and access strips. A negative buffer on a lot with a long thin access strip removes the strip entirely and often splits the remainder. Taking the largest piece is the right default, but silently — without the note the code above records — it produces a figure for a lot that has effectively been redrawn. This is developed further in the guide on handling irregular and flag lots in setback computation.

Split-zoned parcels. When a parcel spans two districts, capacity is computed per portion and the results are not additive in general: the coverage cap and FAR may apply to the whole lot while the setbacks apply per portion, depending on the ordinance. Summing per-portion capacity is a specific claim about the ordinance and needs to be justified, not assumed.

Storey height assumptions. Converting a height limit in feet into a number of storeys requires a storey height, which is not in the zoning data. Eleven feet is a common planning convention and it is an assumption; a fourteen-foot ground floor in a mixed-use building changes the answer. This assumption drives unit yield more than almost anything else and belongs in the record.

Unit yield is an estimate about buildings, not about land. Dividing floor area by an average unit size produces a number with a false air of precision. It depends on circulation efficiency, parking, and unit mix, none of which zoning specifies. Publish it as a range with its divisor stated, or publish floor area and let the consumer apply their own assumption — which is usually what they want anyway.

Integration points jump to heading

Capacity analysis sits at the end of several chains and feeds two consumers.

It consumes resolved standards from overlay district modeling and parcel geometry that has passed the CRS gate. Both are hard dependencies: standards read from a base code and geometry in degrees each produce confident wrong numbers rather than errors.

It feeds change detection. When a rezone lands, the interesting output is not that the code changed but that capacity changed — and by how much. Wiring capacity into the change record turns zoning change alerting from “parcel 07-114 changed from R-1 to MU-2” into “…and its allowable floor area rose from 1 100 m² to 4 300 m²”, which is the form a subscriber can act on.

It also feeds map publishing. Capacity is one of the few derived attributes worth putting on a tile layer, because it is what a user actually wants to see shaded — and because it changes on rezone, it is the attribute that most tests the cache-invalidation design in vector tile publishing for zoning maps.

Compliance and audit artifacts jump to heading

Because capacity figures inform priced decisions, the artifacts here are held to a higher standard than elsewhere in the pipeline.

Each capacity record should retain the intermediate geometries — the setback envelope and the post-encumbrance envelope — not merely their areas. A reviewer disputing a figure will want to see the shape, and a stored envelope makes the dispute resolvable in minutes instead of by rerunning a pipeline that may have changed since.

Each record must carry its assumption list and the standard citations from overlay resolution. “Allowable floor area 4 300 m²” is not an auditable statement; “4 300 m², bound by FAR 1.2 from the transit overlay (§18-402), assuming 11 ft storeys, front setback applied on all edges for want of a frontage determination” is. The second one can be checked, challenged, and corrected.

And capacity outputs should be versioned with the standards that produced them. When an ordinance amendment changes a setback, every capacity figure derived under the old standard is now historical rather than wrong, and the only way to keep that distinction legible is to stamp each figure with the effective date of the standards it used — the same bitemporal discipline temporal versioning & snapshots applies to the zoning itself.

Reporting capacity so it survives being quoted jump to heading

Capacity figures get pasted into memos, and a number separated from its assumptions becomes a claim nobody can defend. Three reporting habits prevent most of that damage.

Which constraint binds, by lot type Stacked bar chart of the binding constraint across four lot types, 500 parcels each. On narrow urban infill lots setbacks bind on 431 of 500. On suburban single-family lots lot coverage binds on 388. On mid-rise mixed-use lots floor area ratio binds on 344. On rural lots an easement or buffer binds on 262, the only group where the binding constraint is frequently not a zoning standard at all and therefore not addressable by a variance. Which constraint binds, by lot type 0 200 400 600 431 urban infill 388 70 suburban SF 344 96 52 mid-rise mixed 262 41 104 93 rural Parcels (500 per lot type) easement or buffer floor area ratio lot coverage setbacks An easement is a property right; a setback is a regulatory choice a variance can address.
The binding constraint is a property of the lot, not of the district — which is why a portfolio scan should group by it. The rural column is the one to read carefully: an easement binds on more than half those parcels, and unlike a setback it is a property right that zoning relief does not touch.

Report the binding constraint beside every figure. “Allowable footprint 480 m², bound by lot coverage” invites the right next question; “480 m²” invites the wrong one, which is to treat it as a property of the land rather than of the current ordinance. The binding constraint is also the field that makes a portfolio scan useful — a set of parcels all bound by setbacks is a very different opportunity from a set all bound by FAR.

Report floor area rather than unit yield wherever the consumer will accept it. Floor area follows from the ordinance; unit yield follows from assumptions about circulation, parking and unit mix that zoning does not specify. Where a unit figure is genuinely required, give a range with its divisor stated inline, so a reader who disagrees with the assumption can substitute their own rather than discarding the whole estimate.

And report the standards’ effective date. A capacity figure computed under a superseded ordinance is historical rather than wrong, and the two are indistinguishable without a date. This is what lets a figure quoted in a memo from March still be verified in September: it is not a claim about today’s rules, and it should not read as one.

Sanity checks worth running on every batch jump to heading

Capacity analysis has a small set of checks that catch most implementation errors cheaply, and they are worth running as assertions rather than as review steps.

Front setback against buildable footprint on an 18 by 40 m lot Line chart of buildable footprint in square metres against the front setback in feet, for an 18 by 40 metre urban lot with side setbacks fixed at 8 feet and a rear setback of 20 feet. The geometric footprint falls linearly from about 496 square metres at no front setback to about 392 square metres at a 30 foot setback. A flat line marks the 40 per cent lot coverage cap at 288 square metres, which lies below the geometric limit across the whole range, so coverage is the binding constraint on this lot no matter what the front setback is. Front setback against buildable footprint on an 18 by 40 m lot 0 100 200 300 400 500 0 5 10 15 20 25 30 Front setback (feet) Buildable footprint (m²) footprint the geometry allows 40% lot coverage cap
On this lot the coverage cap sits below the geometric limit at every setback, so the front setback does not bind at all — arguing about it would change nothing. Computed from the offsets in the code above; run this curve per lot shape and the binding constraint becomes obvious rather than assumed.

Buildable area must never exceed lot area, and the intermediate areas must decrease monotonically through the subtraction stages. A stage that increases means a buffer went outward, which is a sign error rather than a data problem. Footprint must not exceed the coverage cap, and floor area must not exceed the lesser of the height-derived and FAR-derived limits — both of which are easy to violate by computing coverage against the setback envelope instead of the lot.

Distribution checks catch the rest. Plot buildable area as a share of lot area across a batch: a healthy distribution is broad, with small urban lots low and large suburban lots high. A spike at exactly zero means setbacks are eliminating lots that should be developable, usually because a frontage determination is missing and the front setback is being applied on all four sides. A spike at exactly the coverage ratio means the geometry stage is being bypassed entirely.

FAQ jump to heading

Can buildable area be computed with a single negative buffer?

Only when front, side and rear setbacks are equal, which is uncommon. A uniform inward buffer applies the same distance to every edge, so using the front setback understates capacity on most lots and using the side setback overstates it. Per-edge offsets need a frontage determination; where none is available, apply the conservative distance and record that the figure is a floor rather than an estimate.

Does the lot coverage limit apply to the lot or to the setback envelope?

To the lot, in almost every ordinance — the cap is a share of the parcel’s total area, not of the area left after setbacks. Applying it to the envelope inflates the permitted footprint on every parcel with generous setbacks, and because the result is still smaller than the envelope it looks entirely reasonable.

How should easements be treated differently from setbacks?

Setbacks are regulatory and a variance can address them; a recorded easement is a property right that zoning relief does not touch. Reporting both in one ‘constraints’ field implies a flexibility that does not exist, so keep them in separate fields and name the encumbrance type — a reader deciding whether to pursue a variance needs to know which kind of obstacle they are looking at.

Is unit yield ever safe to publish?

As a range with its assumptions stated inline, yes; as a single number, no. Unit yield depends on an average unit size, a circulation efficiency and a parking approach, none of which appear in zoning data. Publishing one number transfers those three assumptions to the reader silently, and they will be attributed to the ordinance rather than to you.