Overlay District Modeling

A parcel is zoned R-1. It is also inside a historic-preservation overlay, a wellhead-protection overlay, and an airport-height overlay. Its permitted uses come from R-1, its maximum height from the airport overlay, its facade requirements from the historic overlay, and its prohibition on certain uses from the wellhead overlay. Ask a data model “what is this parcel zoned?” and there is no single honest answer — and the moment a pipeline stores one, it has thrown away most of what governs the lot. This is the modelling problem overlays create: they modify a base designation without replacing it, they compose, they conflict, and they change on their own schedule. Getting them wrong produces compliance answers that are confidently derived from a third of the applicable rules. This topic is part of the Municipal Zoning Data Architecture & Compliance Frameworks area, and it depends directly on the code discipline established by zoning taxonomy mapping.

Prerequisites and operational context jump to heading

Three conditions have to hold before overlays can be modelled rather than merely stored.

Base and overlay must be distinguishable in the source data. Many portals publish a single zoning string that already has the overlay fused into it — R-1-HO, RSF1/WP, R1(A) — and some publish overlays as a separate polygon layer. The two cases need different ingestion paths, and conflating them is how overlays get lost: fused strings look like base codes, so a crosswalk keyed only on base codes maps R-1-HO to whatever R-1 maps to and silently discards the historic overlay. Where the source fuses them, splitting is a parse with a per-jurisdiction rule, not a rename.

Overlay geometry must be independently valid and aligned. Overlay boundaries are drawn by different departments at different times from different basemaps, so they routinely disagree with parcel boundaries by a few metres. That disagreement is not noise to be smoothed away — it decides whether a parcel is in an overlay — and it makes the CRS alignment strategies gate a hard prerequisite rather than a nicety. A one-metre datum error at an overlay edge flips membership for the parcels along it.

Each overlay needs a declared composition semantics. This is the part most often skipped. An overlay does one of a small number of things to the base: it restricts, it permits additionally, it replaces a specific standard, or it adds a procedural requirement. Which one it does is a property of the ordinance, and it must be recorded per overlay, once, by someone who has read the text. No amount of geometry or string processing can infer it.

Architecture: a base plus an ordered set of modifiers jump to heading

The model that works treats a parcel’s zoning as a base designation plus an ordered set of overlays, with the effective standard computed on demand rather than stored. Storing the computed result is tempting and wrong: it cannot be recomputed when an ordinance changes, and it cannot answer “which rule produced this number?”

Resolving one effective standard from a base code and three overlays A resolution diagram for a single parcel. The base R-1 designation supplies permitted uses, a 35 foot height limit, a 25 foot front setback and a 0.5 floor area ratio. Three overlays then apply in a declared precedence order. The airport height overlay replaces the height limit with 28 feet. The historic overlay adds a facade review requirement and does not change any numeric standard. The wellhead protection overlay removes two otherwise permitted uses. The resolved column on the right shows the effective standards with the source of each one named: uses from the base minus the wellhead removals, height from the airport overlay, setback and floor area ratio still from the base, plus a procedural requirement from the historic overlay. Every resolved value carries the rule that produced it, so a compliance answer can cite its source. Base designation R-1 · low-density residential permitted uses: 6 height: 35 ft front setback: 25 ft FAR: 0.50 Overlays, in declared precedence order 1 · Airport height (AH) replaces height → 28 ft replace 2 · Historic preservation (HP) adds facade review; no numeric change procedural 3 · Wellhead protection (WP) removes 2 permitted uses restrict Resolved, with provenance permitted uses: 4 R-1 (6) minus WP (2) height: 28 ft from AH — base 35 ft superseded front setback: 25 ft from R-1 — no overlay touches it FAR: 0.50 · review: HP facade from R-1 · procedural from HP Nothing here is stored as a resolved value. The parcel record holds R-1 + {AH, HP, WP} and the standards are computed when asked, so an ordinance amendment changes every answer without a data migration. The provenance column is what makes a compliance answer defensible: "28 ft" is not useful on its own, but "28 ft, from the airport height overlay, superseding the R-1 limit of 35 ft" can be checked against the ordinance. Precedence is declared per jurisdiction, not inferred. Two overlays that both replace height need an explicit rule — usually "most restrictive wins" — recorded with the ordinance section that says so.
The parcel record stores R-1 + {AH, HP, WP}, never the resolved numbers. Resolution happens on demand and each output value names the rule that produced it, which is what turns "28 ft" into an answer somebody can check against the ordinance text.

The four composition kinds jump to heading

Every overlay in a jurisdiction should be classified into one of four kinds when it is first modelled. The classification is what the resolver dispatches on, and it is stable — an ordinance amendment changes an overlay’s parameters far more often than its kind.

Kind What it does to the base Resolution rule Example
restrict removes permitted uses, or tightens a numeric standard intersect uses; take the more restrictive number wellhead protection removing fuel storage
replace substitutes a specific standard outright overlay value wins for that standard only airport height limit
permit_additional adds uses the base does not allow union of use sets transit-oriented overlay adding ground-floor retail
procedural adds a review or approval requirement no standard changes; a requirement is appended historic facade review

A fifth pseudo-kind is worth naming explicitly because it causes trouble: an overlay that replaces the base entirely is not an overlay, it is a rezone, and modelling it as an overlay produces a parcel with two competing base designations. If the ordinance text says the district “supersedes the underlying zoning,” it belongs in the base timeline, not the overlay set.

Production implementation jump to heading

The resolver below composes a base and an ordered overlay set into effective standards with provenance. It is deliberately data-driven: the composition kinds are declarative, so adding an overlay is a data change rather than a code change.

from dataclasses import dataclass, field
from enum import Enum


class Kind(str, Enum):
    RESTRICT = "restrict"
    REPLACE = "replace"
    PERMIT_ADDITIONAL = "permit_additional"
    PROCEDURAL = "procedural"


@dataclass(frozen=True)
class Overlay:
    code: str
    kind: Kind
    precedence: int                       # declared per jurisdiction, lower wins ties first
    ordinance_ref: str                    # the section that authorises this behaviour
    numeric: dict[str, float] = field(default_factory=dict)
    uses_removed: frozenset[str] = frozenset()
    uses_added: frozenset[str] = frozenset()
    requirements: tuple[str, ...] = ()


@dataclass
class Resolved:
    numeric: dict[str, float]
    uses: set[str]
    requirements: list[str]
    provenance: dict[str, str]            # standard -> what decided it

    def cite(self, standard: str) -> str:
        return self.provenance.get(standard, "unresolved")


# Which direction is "more restrictive" is a property of the standard, not of the
# overlay: a smaller height limit is more restrictive, a LARGER setback is.
MORE_RESTRICTIVE_IS_LOWER = {"height_ft", "far", "units_per_acre", "lot_coverage"}
MORE_RESTRICTIVE_IS_HIGHER = {"front_setback_ft", "side_setback_ft", "rear_setback_ft",
                              "min_lot_area_sf", "parking_spaces_min"}


def resolve(base, overlays: list[Overlay]) -> Resolved:
    out = Resolved(
        numeric=dict(base.numeric),
        uses=set(base.uses),
        requirements=[],
        provenance={k: f"base:{base.code}" for k in base.numeric},
    )
    out.provenance["uses"] = f"base:{base.code}"

    for ov in sorted(overlays, key=lambda o: o.precedence):
        if ov.kind is Kind.PROCEDURAL:
            out.requirements.extend(ov.requirements)
            out.provenance.setdefault("requirements", "")
            out.provenance["requirements"] = (
                out.provenance["requirements"] + f" {ov.code}({ov.ordinance_ref})").strip()
            continue

        if ov.kind is Kind.REPLACE:
            for k, v in ov.numeric.items():
                out.numeric[k] = v
                out.provenance[k] = f"{ov.code}:replace({ov.ordinance_ref})"

        elif ov.kind is Kind.RESTRICT:
            for k, v in ov.numeric.items():
                current = out.numeric.get(k)
                if current is None:
                    chosen = v
                elif k in MORE_RESTRICTIVE_IS_LOWER:
                    chosen = min(current, v)
                elif k in MORE_RESTRICTIVE_IS_HIGHER:
                    chosen = max(current, v)
                else:
                    raise ValueError(
                        f"standard {k!r} has no declared restrictiveness direction; "
                        f"add it before {ov.code} can restrict it")
                if chosen != current:
                    out.numeric[k] = chosen
                    out.provenance[k] = f"{ov.code}:restrict({ov.ordinance_ref})"
            if ov.uses_removed:
                removed = out.uses & ov.uses_removed
                out.uses -= ov.uses_removed
                if removed:
                    out.provenance["uses"] += f" −{ov.code}"

        elif ov.kind is Kind.PERMIT_ADDITIONAL:
            added = ov.uses_added - out.uses
            out.uses |= ov.uses_added
            if added:
                out.provenance["uses"] += f" +{ov.code}"

    return out

The ValueError in the restrict branch is intentional and load-bearing. A standard whose restrictiveness direction has not been declared cannot be composed safely — guessing that “smaller is stricter” is right for height and wrong for setbacks — so the resolver refuses rather than producing a number. Every standard the pipeline handles must appear in one of the two direction sets, and adding one is a deliberate act.

Edge cases and gotchas jump to heading

Two overlays replace the same standard. The airport overlay caps height at 28 feet; a scenic-corridor overlay caps it at 32. Precedence order decides which wins, and precedence must come from the ordinance rather than from insertion order. Where the ordinance is silent — which happens — the defensible default is most-restrictive-wins, recorded as a policy decision with its own reference so that the choice is visible rather than accidental.

An overlay covers part of a parcel. Overlay boundaries follow creeks, contour lines, and airport approach cones; parcels do not. A parcel half inside a wellhead overlay is genuinely half inside it, and the correct model is a split, not a majority vote. This is exactly the split-zoning case that spatial overlay analysis produces multiple rows for, and flattening it to “mostly not in the overlay” is how a prohibited use gets approved on the half of the lot where it is prohibited.

An overlay changes while the base does not. The base code is unchanged, so a change detector keyed on the zoning string reports nothing — while the parcel’s permitted uses have just changed. Overlay membership has to participate in the change hash, or the whole class of overlay-only amendments is invisible. This is the single most common way overlay modelling fails in production, and it fails silently.

Overlays with their own effective dates. An overlay adopted in March against a base in force since 2019 gives the parcel two independent timelines. Resolution is therefore always as of a date, and the overlay set has to be time-filtered before composition — which means overlay membership belongs in the bitemporal history described by municipal data structures, not in a current-state join table.

Fused codes that are not overlays. R-1A is often a distinct base district, not R-1 plus an “A” overlay. Splitting it produces an overlay that does not exist and a base that means something different. The split rule has to be per-jurisdiction and validated against the ordinance’s district list, not applied as a general pattern.

Integration points jump to heading

Overlay modelling sits between taxonomy mapping and rule evaluation, and it has an interface with each.

Upstream, zoning taxonomy mapping resolves the base code, and the same jurisdiction-scoped discipline applies to overlay codes: HO means historic overlay in one city and homeowner-association overlay in the next. Overlay crosswalks are keyed on (jurisdiction, overlay_code) for exactly the reasons base crosswalks are.

Downstream, compliance framework integration consumes resolved standards. The interface is the Resolved object with its provenance, not a flattened row — a rule engine that receives “height limit 28” without knowing it came from the airport overlay cannot explain its own output, and explanation is most of what a compliance answer is for.

The change-detection interface runs the other way. Change detection & geometry diffing needs the overlay set inside the canonical change hash, so an overlay-only amendment produces a change event. The hash input is the sorted overlay code set plus the base code, which keeps it stable against ordering differences in the source.

Compliance and audit artifacts jump to heading

Overlay resolution produces one artifact that matters more than the rest: the citation trail for each effective standard. A resolved height of 28 feet with AH:replace(§14-207) attached can be checked against the ordinance by someone who does not trust your pipeline, which is the only test of an audit artifact that counts.

Alongside it, the overlay registry — code, kind, precedence, ordinance reference, adopting date, and the person who classified it — is the record that makes the model reviewable. A registry entry asserting that an overlay restricts rather than replaces is a legal reading, and it should be attributable to whoever made it.

Finally, split-overlay records need to survive as splits. When a parcel is partly inside an overlay, the audit trail should show two resolved results with their respective areas rather than one flattened answer, because the flattened answer cannot be reconstructed into the truth and the split can always be summarised.

Building the overlay registry jump to heading

The registry is the piece of this design that cannot be generated, and it is what makes everything else possible. Each entry records one overlay’s code, its jurisdiction, its composition kind, its precedence, the ordinance section that authorises its behaviour, its adopting date, and the person who classified it. Roughly a dozen entries cover a mid-sized city, and building them is an afternoon’s reading of the zoning ordinance rather than an engineering task.

What goes wrong when a registry field is guessed instead of read A five-row by three-column matrix of overlay registry fields. Rows are the composition kind, the precedence order, the ordinance reference, the adopting date, and the restrictiveness direction of each numeric standard. Columns are where the field must come from, what happens if it is inferred from the data instead, and whether the resulting error is visible in the output. Every field must come from the ordinance text, every inference produces a specific wrong answer, and none of the errors is visible in the output. What goes wrong when a registry field is guessed instead of read must come from if inferred instead error visible in the output? Composition kind ordinance text transit overlays read as restrictive no Precedence order ordinance text depends on the portal row order no Ordinance reference ordinance text output cannot be checked at all no Adopting date adopting resolution overlay applied to the wrong era no Restrictiveness direction the standard itself setbacks minimised, heights maximised no read it once a specific wrong answer
The third column is the same in every row, and it is why the registry cannot be generated. Inferring the kind from an overlay's name gets transit overlays backwards; inferring precedence from row order makes composition depend on a portal's sort. Both produce clean, confident, wrong standards.

That afternoon is worth defending, because every shortcut around it fails in the same way. Inferring a kind from the overlay’s name produces restrict for anything containing “protection” and gets transit overlays exactly backwards. Inferring precedence from the order overlays appear in the source data makes composition depend on a portal’s row ordering, which changes without notice. And omitting the ordinance reference means the resolver’s output cannot be checked, which removes the property that made resolution worth computing rather than storing.

A useful discipline is to treat an unregistered overlay as a hard failure rather than as an unknown. When a parcel arrives carrying an overlay code with no registry entry, the correct output is not “base standards apply” — that is the most permissive possible answer and it is being produced by ignorance. Route the parcel to review, exactly as an unmapped base code is routed, and the registry gets completed by the arrival of real data rather than by an attempt to anticipate everything.

Overlay change detection in practice jump to heading

Because overlays modify without replacing, an overlay-only amendment is invisible to any change detector keyed on the base code alone. The fix is small and needs to be deliberate: the canonical change hash for a parcel must include the sorted set of overlay codes alongside the base code and the effective date. Sorting matters, because a source that returns overlays in a different order on two consecutive runs would otherwise produce a change event on every run.

A year of zoning amendments, split by what they touched Stacked bar chart of zoning amendments by quarter across one year, split into amendments that changed a base code, amendments that changed only overlay membership, and amendments that changed both. Overlay-only amendments are the largest category in three of the four quarters, totalling 61 of the 104 amendments for the year. A change detector keyed on the base code alone reports none of them. A year of zoning amendments, split by what they touched 0 10 20 30 40 3 14 7 Q1 5 19 6 Q2 11 9 Q3 4 17 7 Q4 Amendments adopted both base and overlay overlay membership only base code only Overlay-only amendments are invisible to a detector keyed on the base code.
Overlay-only amendments are the majority here — 61 of 104 for the year — and a change detector keyed on the base code reports exactly none of them. This is the argument for putting the sorted overlay set inside the canonical change hash: without it, most of the year's regulatory change is invisible to the pipeline.

The consequence is that overlay adoption produces the same class of change record as a rezone, and it should. From a landowner’s point of view, having two permitted uses removed by a wellhead overlay is not meaningfully different from a rezone that removes them — and a pipeline that reports the second while staying silent about the first is reporting the paperwork rather than the regulation.

FAQ jump to heading

Should the resolved standards be stored or computed on demand?

Computed, with the base code and overlay set stored. Storing resolved values means an ordinance amendment requires a data migration across every affected parcel, and it destroys the provenance that makes a compliance answer checkable. Resolution is cheap — a dictionary merge over a handful of overlays — so the only argument for storing it is query convenience, which a materialised view can supply without losing the inputs.

What if two overlays replace the same standard and the ordinance is silent on precedence?

Apply most-restrictive-wins and record that choice as a policy decision with its own reference, so the reasoning is visible rather than accidental. The important part is that the tie-break is declared somewhere a reviewer can find it; an implicit tie-break hidden in insertion order produces answers that change when a portal reorders its rows.

How do I model an overlay that supersedes the underlying zoning entirely?

As a base designation, not an overlay. If the ordinance text says the district replaces the underlying zoning, then the parcel’s base changes on the adopting date and belongs in the base timeline. Modelling it as an overlay leaves the parcel with two competing base designations and no rule for choosing between them.

What happens when an overlay covers only part of a parcel?

The parcel is genuinely split, and the honest model is two resolved results with their respective areas. A majority-area rule is a simplification that will eventually approve a prohibited use on the portion where it is prohibited. Splits can always be summarised for display; a flattened answer cannot be recovered into the truth.