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?”
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.
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.
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.
Related jump to heading
- Section overview: Municipal Zoning Data Architecture & Compliance Frameworks
- Zoning Taxonomy Mapping — overlay codes need the same jurisdiction-scoped crosswalk that base codes do
- Compliance Framework Integration — consumes resolved standards, and needs their provenance to explain itself
- Development Capacity & Buildable Area — capacity computed from a base code ignores whichever overlay caps the height
- Change Detection & Geometry Diffing — why the overlay set has to sit inside the canonical change hash