← Back to series overview

The Data Layer: Authoritative Sources Wrapped as Guarded Tools

A due-diligence report is only as defensible as its sources. So this part is about going to the systems of record directly, citing them, and making sure a confused agent physically cannot ask for the wrong thing at scale.

What you'll understand after Part 4

  • Which authoritative sources feed the report and how each one is accessed
  • Pattern 1: every external call is wrapped in a guarded tool, never handed to the model raw
  • Pattern 2: sources are ranked, county record beats commercial aggregate beats web page
  • How the municipal ordinance corpus is built: PDFs to Document Intelligence to section chunks to hybrid search
  • Why retrieval is always scoped to the parcel's actual jurisdiction path

Table of Contents

Go to the systems of record

Back to the customer for a second: when a lender asks "where did this flood zone come from?", the answer has to be a citation, not an archaeology project. So the pipeline goes to the systems of record directly and cites them, never a derived copy, never a summary someone else made.

That sounds obvious until you're tempted by a convenient aggregator that has everything in one API. The aggregator is fine as a source, as long as it is labeled as one and ranked below the county record it was derived from. Ranking is Pattern 2 below.

The sources and their access patterns

SourceWhat it providesAccess pattern
Regrid parcel APIParcel records, geometry, ownership, zoning codesGuarded resolver: field allowlist, input sanitization, count-gate before large pulls, token from Key Vault
Municipal ordinance corpusZoning rules, dimensional standards, parking, signage, feesPDFs → Document Intelligence layout → section chunks with jurisdiction taxonomy → Azure AI Search hybrid retrieval, jurisdiction-scoped
FEMA NFHLFlood zones and panelsDirect feature service query per parcel geometry
Planetary ComputerElevation (3DEP / Copernicus DEM), land cover, soils rastersServer-side TiTiler statistics, no local raster processing
USDA Soil Data AccessSoil map units and interpretationsLive SQL query service
FWS National Wetlands InventoryWetland featuresFeature query with land-cover screening fallback
Azure MapsGeocoding, address backfill, disambiguation thumbnailsEntra-authenticated REST
Web research (fallback)Utility providers and contacts onlyBing-grounded agent, tagged Source.WEB, the weakest provenance tier, corpus-first
Note the Planetary Computer row: terrain and land-cover statistics are computed server-side with TiTiler. The agent never downloads a raster and never runs local geoprocessing. That keeps the container small, the run fast, and the result reproducible from a URL you can cite.

Pattern 1: guarded tools, not raw APIs

Every external call is wrapped in a guarded tool, not handed to the model raw. The Regrid resolver is the clearest example. It sanitizes inputs, allowlists the fields it will request, and gates on a count query before any large pull, so a confused agent physically cannot request the wrong thing at scale.

ALLOWED_FIELDS = {"parcelnumb", "owner", "address", "geometry", "zoning", "ll_gisacre"}
MAX_PARCELS = 50

def resolve_parcels(strategy: Strategy, query: str) -> list[ParcelRecord]:
    q = sanitize(strategy, query)                 # strip operators, normalize, validate shape
    n = regrid.count(q)                           # cheap count first
    if n == 0:
        return []                                 # caller records reason_null
    if n > MAX_PARCELS:
        raise TooManyCandidates(n)                # never a 10,000-parcel pull by accident
    return regrid.search(q, fields=ALLOWED_FIELDS, token=keyvault.get("regrid-token"))

The model sees a tool called "resolve parcels" that takes a strategy and a query. It does not see Regrid's query language, it does not see the token, and it cannot ask for fields outside the allowlist. Same idea for every other row in the table: the guard lives in code, the model lives behind it.

Pattern 2: sources are ranked

County record beats commercial aggregate beats web page. That hierarchy is encoded in two places: the divergence recommendation from Part 2 (when two sources disagree, the annotation says which one ranks higher) and the utilities fallback (corpus first, web only if the corpus has nothing).

TierExampleHow it shows up
1. Authoritative recordCounty parcel record, FEMA NFHL, ordinance sectionCITED default tier, drives divergence recommendations
2. Commercial aggregateRegrid-derived zoning codeCITED labeled as aggregate, outranked by tier 1 on conflict
3. Web researchUtility provider contact from a searchSOURCE.WEB explicitly labeled weakest tier, never blended in

Web-sourced values are explicitly labeled as the weakest tier rather than blending in. If a phone number for the water utility came from a web page, the report says so, and the engineer can decide whether to trust it.

The ordinance corpus

Municipal codes arrive as sprawling PDFs, hundreds of pages, inconsistent formatting, cross-references everywhere. The ingestion pipeline handles them in four steps:

Ordinance PDFs
per municipality
→
Document Intelligence
layout → markdown
→
Section chunks
cross-refs + jurisdiction taxonomy
→
Azure AI Search
hybrid keyword + vector
  1. Convert each PDF through Azure Document Intelligence's layout model into structured markdown that keeps headings and tables intact.
  2. Chunk by section, not by token count, and carry the section number, its cross-references and a jurisdiction taxonomy path on each chunk.
  3. Index for hybrid retrieval, keyword plus vector, so "minimum front setback" and "§ 6.4.2" both find the right chunk. Docs: hybrid search in Azure AI Search.
  4. Scope every query to the parcel's actual jurisdiction path, as a filter, not as a prompt hint.
The scoping rule is not optional. Retrieval is always filtered to the parcel's jurisdiction path, so the Trenton agent cannot accidentally cite Cleveland's setback rules. A prompt that says "only use Trenton's code" would get it right most of the time. A filter gets it right every time, and Part 2 already told you which of those two this project cares about.

The constrained extraction step from Part 2 then reads the retrieved chunk and turns it into a value, and the grounding check confirms the value is actually supported by the chunk it cites. Locator on the attribute is the chunk id, so the engineer can open the exact section.

What's next in Part 5

The finale is about trust engineering: the 300+ offline tests, the runtime gates that hard-fail rather than degrade, the Foundry evaluator replaying invariants over live traffic, what all of this changes for the business, six lessons for anyone building an agentic document pipeline, and where the roadmap goes next.

← Part 3: Foundry Agents Continue to Part 5: Trust + Lessons →