Representing planned unit developments as parcel-specific overlays
Ordinance 2024-118 approves a planned unit development on 34 parcels off Redstone Drive. It sets a height limit of 45 feet where the underlying district allows 35, permits ground-floor retail the district does not, requires 1.2 parking spaces per unit instead of 2, and fixes a 12-foot front setback along one internal street only. None of those standards exist anywhere else in the city, and they apply to exactly those 34 parcels. Model it as a base district and your district registry gains a single-use entry for every PUD the city has ever approved — dozens of them. Model it as a normal overlay and its standards leak onto parcels it does not govern. This guide models it as a scoped instance, extending the composition machinery in overlay district modeling.
Diagnosis: why the two obvious modellings both fail jump to heading
As a base district. PUD-118 becomes a district alongside R-1 and MU-2, and the registry acquires one entry per approved development. A mid-sized city has forty or more, each with its own standards table, none reusable. Worse, the PUD usually modifies the underlying district rather than replacing it — parking and height are specified, everything else falls back — so a base-district modelling has to duplicate the underlying standards into the PUD entry, and they then drift when the underlying district is amended.
As a normal overlay. The registry gains PUD as an overlay code with a standards table. But whose standards? Ordinance 2024-118’s are not 2019-042’s. A single overlay code with one standards table cannot represent forty different developments, and keying by code alone means the first PUD’s height limit is applied to every PUD parcel in the city.
The distinguishing property is that a PUD’s standards are instance data, not class data:
def audit_pud_candidates(parcels, registry):
"""Codes that look like a per-development approval rather than a district."""
import re
suspicious = []
for code, n in parcels["base_code"].value_counts().items():
if not re.match(r"^(PUD|PD|SPA|MPD)[-\s]?\d+", code.upper()):
continue
suspicious.append({
"code": code,
"parcels": n,
"in_district_registry": code in registry.districts,
# A district serves many unrelated areas; a PUD serves one contiguous
# development. Contiguity is the giveaway.
"contiguous": is_one_contiguous_group(parcels, code),
})
return suspicious
A code covering one contiguous group of parcels and appearing nowhere else in the city is an approval, not a district.
Step-by-step implementation jump to heading
1. Separate the overlay kind from the overlay instance jump to heading
-- The class: there is exactly one row for PUDs, describing how a PUD composes.
INSERT INTO overlay_registry (code, kind, precedence, ordinance_ref, notes) VALUES
('PUD', 'replace', 0, 'title 14 ch. 9',
'A PUD replaces named standards and leaves the rest to the underlying district. '
'Precedence 0: a site-specific approval outranks area-wide overlays.');
-- The instances: one row per approved development, with its own standards.
CREATE TABLE overlay_instance (
instance_id text PRIMARY KEY, -- 'PUD-2024-118'
overlay_code text NOT NULL REFERENCES overlay_registry(code),
jurisdiction text NOT NULL,
ordinance_ref text NOT NULL, -- 'Ord. 2024-118'
adopted_on date NOT NULL,
expires_on date, -- PUDs can lapse if unbuilt
numeric jsonb NOT NULL DEFAULT '{}',
uses_added text[] NOT NULL DEFAULT '{}',
uses_removed text[] NOT NULL DEFAULT '{}',
requirements text[] NOT NULL DEFAULT '{}',
CONSTRAINT expires_after_adoption CHECK (expires_on IS NULL OR expires_on > adopted_on)
);
-- Membership is per parcel, because a PUD's boundary is the approval's boundary and
-- follows nothing else.
CREATE TABLE overlay_instance_member (
instance_id text NOT NULL REFERENCES overlay_instance(instance_id),
parcel_id text NOT NULL,
-- Some standards apply only to part of a PUD — a setback along one internal
-- street. A sub-area label carries that without a second instance.
sub_area text,
PRIMARY KEY (instance_id, parcel_id)
);
2. Resolve the instance, not the code jump to heading
def overlays_for(parcel_id, as_of, registry, instances) -> list[Overlay]:
"""Area-wide overlays come from geometry; instance overlays come from membership,
and each carries ITS OWN standards."""
out = [registry.overlay(code) for code in registry.area_overlays_for(parcel_id, as_of)]
for inst in instances.for_parcel(parcel_id, as_of):
cls = registry.overlay(inst.overlay_code)
out.append(Overlay(
code=inst.instance_id, # 'PUD-2024-118', not 'PUD'
kind=cls.kind,
precedence=cls.precedence,
ordinance_ref=inst.ordinance_ref, # the approving ordinance, not the chapter
numeric=inst.numeric_for(parcel_id), # honours sub_area
uses_added=frozenset(inst.uses_added),
uses_removed=frozenset(inst.uses_removed),
requirements=tuple(inst.requirements),
))
return out
The code on the resolved overlay is the instance id, which is what makes the citation useful: a resolved height of 45 feet attributed to PUD-2024-118:replace(Ord. 2024-118) can be checked against a specific approval, whereas PUD:replace names nothing.
3. Give PUDs the highest precedence, deliberately jump to heading
A site-specific approval is normally intended to outrank area-wide overlays — that is usually the point of seeking one. Precedence 0 encodes that, and like every precedence it needs a citation rather than an assumption. Where an area-wide overlay is not superseded — an airport height cap generally is not, since it exists for safety reasons outside the zoning bargain — that exception belongs in the precedence table as an explicit row, resolved by the machinery in resolving conflicting overlay stacking rules.
4. Handle expiry and partial build-out jump to heading
def active_instances(instances, parcel_id, as_of):
"""A PUD approval can lapse. An expired instance must stop applying on its
expiry date, at which point the underlying district governs again — which is a
change event nobody publishes, because no data changed."""
return [i for i in instances.for_parcel(parcel_id, as_of)
if i.adopted_on <= as_of and (i.expires_on is None or as_of < i.expires_on)]
Expiry is the subtle failure here: nothing arrives from the portal on the day a PUD lapses, so a pipeline driven only by source changes never notices. The expiry date has to be a scheduled trigger of its own, which makes it one of the few change events generated internally rather than observed.
Verification & testing jump to heading
def test_pud_standards_do_not_leak_between_instances(registry, instances):
a = resolve(BASE_R1, overlays_for("p-in-118", DATE, registry, instances))
b = resolve(BASE_R1, overlays_for("p-in-042", DATE, registry, instances))
assert a.numeric["height_ft"] == 45.0 # Ord. 2024-118
assert b.numeric["height_ft"] == 38.0 # Ord. 2019-042
assert a.cite("height_ft") != b.cite("height_ft")
def test_unspecified_standards_fall_back_to_the_district(registry, instances):
r = resolve(BASE_R1, overlays_for("p-in-118", DATE, registry, instances))
assert r.numeric["rear_setback_ft"] == 20.0 # the PUD is silent on this
assert r.cite("rear_setback_ft") == "base:R-1" # so the district still governs
def test_sub_area_setback_applies_only_where_declared(registry, instances):
inner = resolve(BASE_R1, overlays_for("p-118-inner-street", DATE, registry, instances))
outer = resolve(BASE_R1, overlays_for("p-118-perimeter", DATE, registry, instances))
assert inner.numeric["front_setback_ft"] == 12.0
assert outer.numeric["front_setback_ft"] == 25.0 # district value
def test_expired_pud_stops_applying(registry, instances):
before = resolve(BASE_R1, overlays_for("p-in-lapsed", date(2026, 1, 1), registry, instances))
after = resolve(BASE_R1, overlays_for("p-in-lapsed", date(2027, 1, 1), registry, instances))
assert before.numeric["height_ft"] == 45.0
assert after.numeric["height_ft"] == 35.0 # back to the district
assert after.cite("height_ft") == "base:R-1"
def test_district_registry_has_no_pud_instances(registry):
"""The structural assertion: instances must not pollute the district list."""
assert not [d for d in registry.districts if d.upper().startswith(("PUD", "PD-"))]
That last test is the one that keeps the design from eroding. The path of least resistance when a new PUD arrives is to add it as a district, and this assertion is what makes that visible in review.
Failure recovery jump to heading
PUDs already registered as districts. Migrate each to an instance, move its standards into overlay_instance.numeric, populate membership from the parcels currently carrying the code, and set the parcels’ base code to the underlying district. The underlying district is recoverable from the approving ordinance, which normally recites it — and where it does not, from the parcels’ pre-approval history.
One PUD’s standards applied to another’s parcels. Symptom of resolving by code rather than instance. Fix the resolver, then re-resolve every parcel with a PUD instance; the affected set is exactly overlay_instance_member, so the scope is precisely known.
An expired PUD still applying. Add the scheduled expiry trigger, then re-resolve parcels whose instances have lapsed and emit the change events retroactively with their true effective dates — the reversion to the underlying district is a real regulatory change, and it happened whether or not anything was published.
Frequently asked questions jump to heading
Why not just make each PUD a base district?
Because a PUD modifies rather than replaces: it names a few standards and leaves the rest to the underlying district. Modelling it as a base means duplicating the underlying standards into every PUD entry, and those copies drift the moment the underlying district is amended. It also grows the district registry by one single-use entry per approval, which makes the registry unusable for its actual purpose.
Why does the resolved citation use the instance id rather than "PUD"?
Because a citation has to be checkable. PUD-2024-118:replace(Ord. 2024-118) points a reviewer at one specific approving ordinance; PUD:replace points at a chapter that describes the mechanism and contains none of the numbers. The instance id is what turns the answer from an assertion into something verifiable.
How are standards that apply to only part of a PUD handled?
With a sub-area label on the membership row. A 12-foot setback along one internal street applies to the parcels fronting it, and the instance’s numeric map is keyed by sub-area so the same approval can carry both values. Creating a second instance for the sub-area works too and duplicates the ordinance reference, which then has to be kept in step.
What happens when a PUD expires?
The underlying district governs again, and it is a real change with no source event behind it — nothing arrives from the portal on that date. Expiry therefore needs a scheduled trigger that re-resolves the affected parcels and emits change events, which makes it one of the few events the pipeline generates rather than observes.
Related jump to heading
- Parent topic: Overlay District Modeling
- Section overview: Municipal Zoning Data Architecture & Compliance Frameworks
- Resolving conflicting overlay stacking rules — whether a PUD outranks an airport height cap
- Computing effective standards when three overlays apply — the resolution this plugs into
- Municipal Data Structures — instance data against class data, and why the distinction pays