Handling conditional logic for historic district overlays

Handling conditional logic for historic district overlays requires a spatially aware rule evaluation pipeline that decouples geometric intersection from attribute-based compliance checks. In practice, this means resolving parcel-to-overlay overlaps first, then routing project metadata through tiered conditional matrices. Because historic districts rarely enforce uniform restrictions, a robust system must cascade conditions (e.g., material limits for primary facades, relaxed height limits for rear additions, demolition triggers for pre-1940 structures) while preserving deterministic audit trails for compliance officers.

Core Evaluation Architecture

The evaluation flow operates in three deterministic stages. Each stage gates the next to minimize unnecessary computation and ensure predictable outputs.

  1. Spatial Resolution: Intersect the proposed development footprint with historic overlay polygons. Calculate intersection area, percentage overlap, and sub-district classification. This step relies on vectorized spatial joins rather than iterative point-in-polygon checks.
  2. Attribute Routing: Map project metadata (use type, gross floor area, construction era, facade orientation) to overlay-specific rule matrices. Apply conditional branching only when spatial overlap exceeds a configurable threshold, typically ≥10%.
  3. Rule Execution: Evaluate compliance conditions sequentially or in parallel. Flag violations, generate conditional approvals, or route to manual review when rule confidence falls below acceptable bounds. Complex validations should leverage Async Rule Execution Patterns to prevent synchronous pipeline bottlenecks during high-volume screening.

This architecture aligns with established Rule Engine Design for Zoning & Setback Automation principles, where spatial predicates act as hard gates before attribute evaluation begins. Historic districts frequently reference external preservation frameworks, such as the Secretary of the Interior’s Standards for Rehabilitation, which must be translated into machine-readable conditional statements before ingestion into automated pipelines.

Production Implementation Pattern

The following Python implementation demonstrates a production-ready pattern using geopandas and pandas. It isolates spatial joins, applies conditional rule routing, and returns structured compliance outputs. The code uses vectorized operations for bulk screening and includes explicit threshold filtering to avoid false positives from incidental boundary touches.

import geopandas as gpd
import pandas as pd
from shapely.geometry import Polygon, box
from typing import Dict, List, Any
import logging

logging.basicConfig(level=logging.INFO)

def evaluate_historic_overlay_compliance(
    project_gdf: gpd.GeoDataFrame,
    overlay_gdf: gpd.GeoDataFrame,
    overlap_threshold: float = 0.10
) -> List[Dict[str, Any]]:
    """
    Evaluates project footprints against historic district overlays.
    Returns structured compliance reports per project-overlay intersection.
    """
    if project_gdf.empty or overlay_gdf.empty:
        return []

    # 1. Spatial Resolution: Inner join to capture only intersecting geometries
    intersected = gpd.sjoin(
        project_gdf,
        overlay_gdf,
        how="inner",
        predicate="intersects"
    )

    if intersected.empty:
        logging.info("No spatial intersections found.")
        return []

    # 2. Calculate overlap percentage (intersection area / project area)
    intersected["intersection_area"] = intersected.geometry.intersection(
        intersected["geometry_right"]
    ).area
    intersected["overlap_pct"] = intersected["intersection_area"] / intersected["area"]

    # Filter by configurable threshold
    qualified = intersected[intersected["overlap_pct"] >= overlap_threshold].copy()

    if qualified.empty:
        logging.info("Intersections below overlap threshold.")
        return []

    # 3. Rule Execution: Apply conditional logic based on sub-district & project attributes
    compliance_reports = []

    for _, row in qualified.iterrows():
        report = {
            "project_id": row.get("project_id", "unknown"),
            "overlay_id": row.get("overlay_id", "unknown"),
            "sub_district": row.get("sub_district", "unknown"),
            "overlap_pct": round(row["overlap_pct"], 4),
            "conditions_triggered": [],
            "status": "compliant"
        }

        # Example conditional matrix
        height_limit = row.get("height_limit_ft", 0)
        proposed_height = row.get("proposed_height_ft", 0)
        construction_era = row.get("construction_era", 0)
        is_primary_facade = row.get("is_primary_facade", False)

        if proposed_height > height_limit:
            report["conditions_triggered"].append("height_exceeds_limit")
            report["status"] = "conditional_approval"

        if construction_era < 1940 and row.get("demolition_proposed", False):
            report["conditions_triggered"].append("pre_1940_demolition_review")
            report["status"] = "manual_review"

        if is_primary_facade and row.get("material_change_proposed", False):
            report["conditions_triggered"].append("primary_facade_material_restriction")
            report["status"] = "conditional_approval"

        compliance_reports.append(report)

    return compliance_reports

# --- Usage Example ---
if __name__ == "__main__":
    # Mock project footprint
    projects = gpd.GeoDataFrame(
        {"project_id": ["P-101", "P-102"], "geometry": [box(0, 0, 10, 10), box(50, 50, 60, 60)]},
        crs="EPSG:4326"
    )

    # Mock historic overlay
    overlays = gpd.GeoDataFrame(
        {
            "overlay_id": ["HD-01", "HD-02"],
            "sub_district": ["Core", "Riverside"],
            "height_limit_ft": [35, 30],
            "geometry": [Polygon([(2, 2), (8, 2), (8, 8), (2, 8)]), Polygon([(40, 40), (55, 40), (55, 55), (40, 55)])]
        },
        crs="EPSG:4326"
    )

    reports = evaluate_historic_overlay_compliance(projects, overlays)
    print(reports)

Integrating Preservation Standards & Async Routing

Historic overlay rules are rarely static. Municipalities frequently update material palettes, setback exceptions, and review triggers based on evolving preservation guidelines. To maintain pipeline accuracy, external standards should be versioned and ingested as structured JSON or YAML rule matrices rather than hardcoded conditionals. The GeoPandas spatial join documentation outlines best practices for handling coordinate reference system (CRS) alignment and topology validation before rule evaluation begins.

When scaling to municipal or regional datasets, synchronous evaluation becomes a bottleneck. Offloading heavy spatial intersections and multi-step compliance validations to background workers ensures API responsiveness. Implementing Async Rule Execution Patterns allows planners to submit batch screenings, receive immediate acknowledgment tokens, and poll for structured compliance reports once the spatial and attribute pipelines complete.

The Three Questions a Historic Overlay Asks

Historic overlays are the most conditional part of most zoning codes, and the conditions rarely reduce to a single test. Three questions have to be answered in order, and each has a different authoritative source.

The three questions a historic overlay asks, in orderDesignation is checked against the adopted list, contributing status against the survey, and scope against the application; an unsurveyed structure returns indeterminate rather than a guess.Is the parcel in an adopted district?the adopted list governs, not the drawn polygonIs the structure contributing?from the survey, with its date; never inferred from ageIs the proposed work in scope?a property of the application, not of the geographyReview required, exempt, or indeterminatethree outcomes, because unsurveyed is common
Each question has a different authoritative source, and only the first is geographic at all.

Is the parcel in the district? For historic designations this is usually a question about a list rather than a polygon, because districts are typically adopted parcel by parcel and the mapped boundary is drawn around the list afterwards. Where the two disagree — and on boundary parcels they routinely do — the list governs.

Is the structure contributing? Most district ordinances distinguish contributing structures, which carry the full review requirements, from non-contributing ones, which carry a reduced set. This status is a property of the building, not the parcel, it is assigned by a survey with a date on it, and it changes: a structure can be reclassified after a resurvey, and a new building on a district parcel is non-contributing by definition.

Is the work in scope? Ordinary maintenance is generally exempt; alteration of a visible facade generally is not. This is a property of the application, not of the geography at all, which is the part that most often catches out a purely spatial pipeline — the parcel and the structure can both be in scope while the proposed work is not.

def historic_review_required(parcel_id, structure_id, work_scope, registry):
    """Three ordered checks, each against its own authoritative source."""
    listing = registry.designation_for(parcel_id)      # the adopted list, not the polygon
    if listing is None:
        return False, "parcel not in an adopted historic district"
    status = registry.contributing_status(structure_id, as_of=listing.effective_from)
    if status == "unknown":
        # Never infer contributing status from age or geometry.
        return None, "contributing status not surveyed — route to review"
    if work_scope in listing.exempt_scopes:
        return False, f"scope '{work_scope}' is exempt under {listing.citation}"
    return True, f"{status} structure, scope '{work_scope}' under {listing.citation}"

Note the three-valued return: True, False, and None for “cannot determine”. An unsurveyed structure is common in districts designated decades ago, and inferring its status from building age or footprint is exactly the kind of plausible guess that produces an indefensible result.

Keeping the Conditional Work Off the Hot Path

Historic checks are attribute lookups against registries, not geometric computations, which makes them fast individually and slow in aggregate if each one is a separate round trip. In an asynchronous pipeline the temptation is to fetch per parcel inside the worker; the effect is a queue whose throughput is bounded by registry latency rather than by evaluation cost.

Loading the designation registry once per runThe coordinator loads an immutable registry snapshot and ships it to workers with the rule pack, turning each designation check into an in-memory lookup.CoordinatorRegistryWorkerread designations and survey status, as of dateimmutable snapshot (a few hundred kB)dispatch chunk + registry + rule packverdicts; no external reads during evaluation
Per-parcel registry queries bound throughput by registry latency and make the run irreproducible if the registry changes mid-flight.

Load the registry once per run instead, as an immutable snapshot keyed by parcel and structure id, and hand it to the workers along with the rule pack. The registry is small — a district of a few thousand structures is a few hundred kilobytes — and having it in memory turns each check into a dictionary lookup. It also makes the run reproducible, since a registry updated mid-run cannot change the answers of parcels evaluated after the update.

Where the registry genuinely must be queried live, batch the lookups at the coordinator before dispatch and attach the results to the messages. Both approaches share the property that matters: the worker receives everything it needs, so its verdict depends only on its inputs.

Verifying the Conditional Paths

Conditional logic fails silently more often than unconditional logic, because the wrong branch still returns a plausible answer. The defence is a fixture set with one parcel per path, asserted end to end.

Six fixtures that cover every conditional pathOne parcel per branch of the conditional logic, including the two boundary cases where the adopted list and the mapped polygon disagree.ExercisesExpected outcomeContributing, work in scopeThe full review pathReview required, citation attachedContributing, exempt workThe scope exemptionNo review, exemption namedNon-contributing structureThe reduced standardReduced review set appliedUnsurveyed structureThe indeterminate branchRouted to review, reason recordedAdjacent but outsideThe negative caseNo historic rules appliedList and polygon disagreeList precedenceList governs; disagreement logged
Conditional logic fails silently because the wrong branch still returns a plausible answer. One fixture per path is the cheapest defence.

Six fixtures cover the space: a contributing structure with in-scope work, a contributing structure with exempt work, a non-contributing structure in the district, a structure with unsurveyed status, a parcel adjacent to but outside the district, and a parcel on the boundary where the list and the mapped polygon disagree. The last two are the ones that catch real regressions, because they exercise the difference between the authoritative list and the convenience geometry.

Assert on the whole verdict rather than on the boolean: the citation, the status used and the reason string are what a reviewer reads, and a test that only checks true or false will pass while the explanation is wrong.

Frequently Asked Questions

Should the mapped district polygon be used at all?

Yes, but for screening rather than for the decision. Intersecting the polygon narrows a county-wide run to a few thousand candidate parcels cheaply; the adopted list then decides which of those are actually designated. Using the polygon alone means disagreeing with the ordinance on precisely the boundary parcels most likely to be appealed.

How should a resurvey that changes contributing status be handled?

As a dated fact, exactly like a rule amendment. Store the status with an effective date, select it by the application date, and keep the superseded record. Re-running an old application should reproduce the status that applied at the time, not today’s.

What if a structure sits across two parcels, one designated and one not?

Route it to review. This is a genuine ambiguity in most ordinances rather than a computation the pipeline can settle, and producing a confident verdict either way misrepresents the situation. Record both parcels and both designations in the review record so the planner has what they need.

Can these checks run synchronously if the district is small?

Certainly. The asynchronous machinery earns its place at county scale; a single district of a few hundred parcels evaluates inline in under a second. Keep the same evaluator and change only the dispatch, so the answers are identical whichever path is used.

Part of: Async rule execution patterns

Compliance & Audit Requirements

Automated historic district screening must satisfy regulatory transparency standards. Every conditional evaluation should produce an immutable decision log containing:

  • Input Snapshot: Project geometry hash, metadata payload, and overlay version ID at evaluation time.
  • Rule Trace: Exact conditions evaluated, threshold values applied, and pass/fail outcomes per condition.
  • Confidence Flags: Indicators for low-confidence matches (e.g., boundary grazing, incomplete project metadata) that trigger mandatory human review.
  • Output Format: Machine-readable JSON or XML that integrates directly with permitting systems and public disclosure portals.

By enforcing strict separation between spatial resolution, attribute routing, and rule execution, development teams can iterate on preservation logic without destabilizing core GIS operations. This modular approach ensures that handling conditional logic for historic district overlays remains deterministic, auditable, and scalable across jurisdictional boundaries.