Skip to content

Extend buffer apportionment to non-hierarchical aggregation layers (Place, CBSA, CSA, UAC) #370

Description

@irees

Follow-up to #315 / PR #352.

Background

PR #352 ships stop statistical radius apportionment for individual results tables (Stops/Routes/Agencies) and for the aggregation table at the State, County, and Tract layers. The aggregation rollup works via a FIPS prefix match — Pass F returns tract-level intersections with the union of stop buffers, and geographiesForAggregationRow filters those tracts by GEOID prefix to populate each county/state row.

The prefix trick only works when the aggregation layer is a strict GEOID prefix of tract — i.e. State, County, Tract. For Place, CBSA, CSA, UAC20, and FTA-UAC20-Nonurban the GEOIDs are not prefixes (a place can span counties, a CBSA is composed of counties not tracts), so the rollup can't be done client-side without polygon math, which is banned by CLAUDE.md.

As a stopgap, PR #352 disables those non-hierarchical layer choices in the aggregation pickers whenever stopBufferRadius > 0 and surfaces a warning banner. Users have to either switch to a hierarchical layer or set the radius to 0.

Goal

Restore Place / CBSA / CSA / UAC aggregation when buffer apportionment is on, with the same per-tract resolution we get today for County rows. The math we want per aggregation row is:

row demographics = Σ over tracts t: tract_value × (place ∩ buffer ∩ tract area / tract area)

so that density is assumed uniform within each tract (matching the Frequent Transit Service Study calculation referenced in #315) rather than uniform within the whole place/CBSA.

Proposed approaches

Two backend options, both in transitland-server. We don't have a strong preference yet — would like input.

A. Filter by another geography's ID

Extend census_geographies(where: CensusGeographyFilter) to accept a containing-geography filter, e.g.:

where: { dataset: \"tiger2020\", layer: \"tract\", radius: 402, within_geography_id: <place_id> }

The resolver would intersect (buffer ∩ tract ∩ place) server-side via PostGIS and return per-tract rows. The client would call this once per visible place row, or batch by passing an array of containing IDs.

Pros: minimal schema surface, reuses existing types, aggregation precision matches today's per-row math.

Cons: one query per aggregation row (could be dozens), or backend has to support an array variant.

B. Re-upload the selected layer as a multi-polygon clip

Allow the caller to POST a GeoJSON multi-polygon as the spatial filter and run the existing stop_buffer ∩ tract ∩ intersection against it. Same shape as today's admin-boundary path.

Pros: doesn't require the server to host the layer being clipped against — caller supplies it.

Cons: payload size for fine-grained layers, duplication of polygon data the server already has.

Acceptance criteria

  • With stopBufferRadius > 0 and aggregation layer set to Place / CBSA / CSA / UAC / FTA-UAC20-Nonurban, the aggregation table shows the same buffer-apportioned demographic columns + % Area within Stop Radius that today's State/County/Tract rows show.
  • Disable-and-warn UI added in PR Stop statistical radius #352 (report.vue banner, picker option gating in filter.vue and report.vue) can be removed.
  • No client-side geometry math required (CLAUDE.md "Geometry operations: server-side only").

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions