Computing effective standards when three overlays apply
Parcel 0714-22-1 is zoned R-1 and sits inside three overlays: airport height, historic preservation, and wellhead protection. A client asks a simple question — can they build a 32-foot duplex with a small fuel store? — and answering it correctly requires resolving eleven standards across four sources, then being able to show which rule produced each one. This guide works that resolution end to end, because the composition model in overlay district modeling is easiest to get wrong in the arithmetic rather than in the design.
Diagnosis: laying out the inputs before resolving anything jump to heading
Write the inputs down first. Most resolution errors are not logic errors; they are a standard read from the wrong source because nobody enumerated the sources.
| Source | Kind | Contributes |
|---|---|---|
| R-1 (base) | — | 6 permitted uses; height 35 ft; front 25 ft, side 8 ft, rear 20 ft; FAR 0.50; coverage 40% |
| AH — airport height | replace |
height 28 ft |
| HP — historic preservation | procedural |
facade review; no numeric standard |
| WP — wellhead protection | restrict |
removes fuel storage and auto repair; coverage 30% |
Two observations decide the whole resolution. AH replaces height, so the base’s 35 feet is superseded rather than compared — a restrict would have taken the stricter of the two, which happens to be the same number here and would not be in general. And WP restricts coverage, so 30% and 40% are compared and the stricter wins, which for coverage means the lower value.
BASE = Base(
code="R-1",
uses={"single_family", "duplex", "accessory_dwelling", "home_office",
"fuel_storage", "auto_repair"},
numeric={"height_ft": 35.0, "front_setback_ft": 25.0, "side_setback_ft": 8.0,
"rear_setback_ft": 20.0, "far": 0.50, "lot_coverage": 0.40},
)
OVERLAYS = [
Overlay("AH", Kind.REPLACE, precedence=1, ordinance_ref="§14-207",
numeric={"height_ft": 28.0}),
Overlay("HP", Kind.PROCEDURAL, precedence=2, ordinance_ref="§12-118",
requirements=("facade review by the historic commission",)),
Overlay("WP", Kind.RESTRICT, precedence=3, ordinance_ref="§18-402",
numeric={"lot_coverage": 0.30},
uses_removed=frozenset({"fuel_storage", "auto_repair"})),
]
Step-by-step implementation jump to heading
1. Resolve in precedence order, recording a citation per standard jump to heading
def resolve(base, overlays):
out = Resolved(numeric=dict(base.numeric), uses=set(base.uses),
requirements=[], provenance={})
for k in base.numeric:
out.provenance[k] = f"base:{base.code}"
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["requirements"] = (
out.provenance.get("requirements", "") + f" {ov.code}({ov.ordinance_ref})"
).strip()
elif 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():
chosen = stricter(k, out.numeric[k], v)
if chosen != out.numeric[k]:
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}({ov.ordinance_ref})"
return out
2. Read the resolved result jump to heading
Running that on the inputs above gives:
| Standard | Value | Provenance |
|---|---|---|
| permitted uses | 4 | base:R-1 −WP(§18-402) |
| height_ft | 28.0 | AH:replace(§14-207) |
| front_setback_ft | 25.0 | base:R-1 |
| side_setback_ft | 8.0 | base:R-1 |
| rear_setback_ft | 20.0 | base:R-1 |
| far | 0.50 | base:R-1 |
| lot_coverage | 0.30 | WP:restrict(§18-402) |
| requirements | facade review | HP(§12-118) |
Four of the eight rows still come from the base, which is the normal case and is worth noticing: overlays touch a minority of standards, and a resolver that rewrote everything would be hiding that.
3. Answer the client’s actual question jump to heading
def can_build(proposal, resolved) -> tuple[bool, list[str]]:
reasons = []
if proposal.use not in resolved.uses:
reasons.append(f"use {proposal.use!r} is not permitted "
f"({resolved.cite('uses')})")
if proposal.height_ft > resolved.numeric["height_ft"]:
reasons.append(f"height {proposal.height_ft} ft exceeds "
f"{resolved.numeric['height_ft']} ft "
f"({resolved.cite('height_ft')})")
if proposal.coverage > resolved.numeric["lot_coverage"]:
reasons.append(f"coverage {proposal.coverage:.0%} exceeds "
f"{resolved.numeric['lot_coverage']:.0%} "
f"({resolved.cite('lot_coverage')})")
return (not reasons), reasons + [f"also required: {r}" for r in resolved.requirements]
For the 32-foot duplex with fuel storage the answer is no, for two independent reasons: the height exceeds the airport overlay’s 28-foot cap (AH:§14-207), and fuel storage was removed by the wellhead overlay (WP:§18-402). The duplex use itself is fine, and a facade review would be required regardless. That is four distinct facts, each traceable to a section — which is what makes the answer defensible rather than merely correct.
4. Keep the resolution as an artifact jump to heading
def resolution_record(parcel_id, base, overlays, resolved, as_of):
return {
"parcel_id": parcel_id,
"as_of": as_of.isoformat(),
"base_code": base.code,
"overlay_codes": sorted(o.code for o in overlays),
"numeric": resolved.numeric,
"uses": sorted(resolved.uses),
"requirements": resolved.requirements,
"provenance": resolved.provenance, # the part an auditor reads
"resolver_version": RESOLVER_VERSION, # so a rule change is attributable
}
Verification & testing jump to heading
def test_worked_example():
r = resolve(BASE, OVERLAYS)
assert r.numeric["height_ft"] == 28.0
assert r.cite("height_ft") == "AH:replace(§14-207)"
assert r.numeric["lot_coverage"] == 0.30
assert r.cite("lot_coverage") == "WP:restrict(§18-402)"
assert r.numeric["front_setback_ft"] == 25.0
assert r.cite("front_setback_ft") == "base:R-1" # untouched by any overlay
assert r.uses == {"single_family", "duplex", "accessory_dwelling", "home_office"}
assert r.requirements == ["facade review by the historic commission"]
def test_order_of_the_overlay_list_does_not_matter():
import itertools
results = [resolve(BASE, list(p)) for p in itertools.permutations(OVERLAYS)]
assert all(r.numeric == results[0].numeric for r in results)
assert all(r.uses == results[0].uses for r in results)
def test_every_value_has_a_citation():
r = resolve(BASE, OVERLAYS)
for standard in r.numeric:
assert r.cite(standard) != "unresolved"
def test_the_clients_question():
r = resolve(BASE, OVERLAYS)
ok, reasons = can_build(Proposal(use="fuel_storage", height_ft=32, coverage=0.28), r)
assert not ok
assert any("height 32" in x for x in reasons)
assert any("fuel_storage" in x for x in reasons)
The permutation test is the one worth keeping permanently: it proves the resolution does not depend on the order overlays arrive in, which is exactly the property that fails first when somebody adds a fourth overlay kind.
Failure recovery jump to heading
A resolved value with no citation. The provenance map has a gap, which means a standard was written by a path that did not record its source. Treat every answer derived from that resolver version as unverifiable and re-resolve after fixing the gap — an uncited number cannot be defended even if it happens to be right.
A standard that changed after an ordinance amendment. Because nothing is stored resolved, the fix is to update the registry entry and re-resolve; no data migration is needed. Any compliance answer already issued under the old standard remains correct as of its date, which the as_of and resolver_version fields are there to show.
A fourth overlay arriving on the parcel. Resolution handles it without code changes provided its kind and precedence are registered. If it is a second replace on a standard another overlay already replaces, the conflict machinery in resolving conflicting overlay stacking rules takes over.
Frequently asked questions jump to heading
Does the order of the overlay list matter?
Not for the result, which is the property the permutation test protects, but only because precedence is declared on each overlay rather than taken from list position. Sorting by declared precedence before applying is what makes the resolution independent of how the source happened to return the overlay array.
Why does the coverage end up at 30% and the height at 28 ft by different routes?
Because the overlays act differently. WP restricts coverage, so 30% and the base’s 40% are compared and the stricter wins. AH replaces height, so the base’s 35 ft is superseded outright rather than compared. Here both routes happen to produce the value from the overlay; with a base height of 24 ft, the replace would have raised the limit to 28 while a restrict would have kept 24.
Should the resolved record be stored per parcel?
Store it as an artifact of an answer given, not as the parcel’s state. When you tell a client something, record the resolution with its as_of date and resolver version so the advice is reproducible. The parcel itself continues to hold only the base code and overlay set, so an ordinance amendment does not require rewriting parcels.
What if a procedural overlay also has a numeric standard?
Then it is not purely procedural, and its registry entry is wrong. Split it: register the numeric part with the kind that describes what it does to the standard, and keep the procedural requirement alongside. A single overlay doing two kinds of thing is the case where a resolver silently applies only one of them.
Related jump to heading
- Parent topic: Overlay District Modeling
- Section overview: Municipal Zoning Data Architecture & Compliance Frameworks
- Resolving conflicting overlay stacking rules — what happens when two overlays replace the same standard
- Compliance Framework Integration — the rule engine that consumes this result and its citations
- Development Capacity & Buildable Area — turning these standards into a buildable envelope