Skip to content

Suggested house counts from Toronto Open Data - #23

Open
AbeerDas wants to merge 3 commits into
feat/maps-routes-pagefrom
feat/house-count
Open

Suggested house counts from Toronto Open Data#23
AbeerDas wants to merge 3 commits into
feat/maps-routes-pagefrom
feat/house-count

Conversation

@AbeerDas

Copy link
Copy Markdown
Contributor

Summary

Stacked on #17. Adds a house-count suggestion for routes, computed from the City of Toronto's address register — a suggestion the user accepts, never a silent overwrite. house_count_override in the schema was reserved for exactly this ("only relevant once auto-calc exists").

Approach

Each route endpoint (a street address or an intersection — both carry coordinates) snaps to the nearest known address on that street; everything between the two resulting house numbers is counted, filtered to the route's side. Computed on read — nothing is stored, so there's no freshness state to manage.

Side matching uses Toronto's own L/R centreline flag as ground truth and only decides which compass direction each side corresponds to, from the group's mean offset relative to the other group (not against an absolute chord — a route whose endpoints are two houses on the same side would otherwise collapse the sign test). This correctly handles both diagonal streets (Kingston Rd) and curved ones (Glen Manor Dr, which bends around a ravine).

PostGIS turned out unnecessary — the address data already carries centreline side, so it's a house-number range plus a bounding box.

What's here

  • supabase/migrations/20260728000000_toronto_address_points.sql — reference table, RLS + explicit service_role grant (new tables aren't auto-exposed)
  • scripts/load-toronto-addresses.ts (pnpm load-addresses) — streams the ~525k-row CSV via COPY, filters unbuilt (RESERVED) lots, scoped to Beach Metro's area by default (--bbox/--all to widen)
  • lib/services/house-count.ts — the pure matching/geometry core (no DB, no I/O — fully unit-testable)
  • lib/services/routes.tssuggestRouteHouseCount, exact-street-match first (Toronto splits direction suffixes like "Glen Manor Dr W" into separate streets with their own numbering)
  • GET /api/routes/{id}/suggested-house-count
  • Route detail panel: suggested count + Use button (writes houseCount and clears houseCountOverride — writing only the former is broken whenever an override exists, see the seed route 7 regression test) + collapsed address list + required OGL–Toronto attribution

Verification

  • 21 new unit tests (tests/house-count.test.ts) — parity self-check, diagonal streets, curvature, digitization-flip guard, dedup, symmetry
  • 3 new integration tests, self-skipping if pnpm load-addresses hasn't run
  • Live-verified against all 7 seeded routes: every one-sided route returns parity-consistent results (all-odd or all-even numbers) — the only correctness signal available without ground truth yet
  • Along the way, corrected the seed's hand-invented route geography: some coordinates didn't sit on their real streets, and two routes had a side that's geometrically impossible for that street's orientation (a north-south street only has east/west sides)
  • pnpm format:check && pnpm typecheck && pnpm lint && pnpm test all green

Open item

No ground truth yet. Everything above is self-consistency (parity, additivity, symmetry) — real counts for 15–20 routes from Melinda would let us state an actual error bar. Until then the number stays a suggestion.

Contains information licensed under the Open Government Licence – Toronto.

Counts the addresses between a route's two endpoints and offers the number
as a suggestion in the route detail panel. The manual house_count stays
authoritative until someone presses Use.

How it works: each endpoint (a street address or an intersection — both
carry coordinates) snaps to the nearest known address point on the route's
street, and the addresses between those two house numbers are counted,
filtered to the route's side. Computed on read, so nothing is stored and
there is no freshness state to manage.

Side matching uses Toronto's own L/R centreline flag as the source of truth
and only decides which compass direction each group corresponds to, from the
group's mean offset relative to the other group. Comparing the groups to each
other rather than to the start->end chord matters: a route whose endpoints are
two houses on the same side puts the chord along that side, so an absolute
test collapses. Curved streets (Glen Manor Dr) and diagonals (Kingston Rd)
both fall out of this correctly.

House numbering alternates by side, so a one-sided route returning both odd
and even numbers means the side or range logic is wrong. That check ships as
`parityConsistent` on the response and is asserted in the tests — it is the
only correctness signal available until real counts exist to compare against.

PostGIS turned out to be unnecessary. Notable data details: ADDRESS_NUMBER is
text ("2962B") so LO_NUM is the range key; MAINT_STAGE 'RESERVED' rows are
allocated but unbuilt and are dropped at load; the street match is exact
first, because Toronto's direction-suffixed siblings (Glen Manor Dr E/W) are
separate streets with their own numbering.

Also corrects the seeded route geography, which used hand-invented
coordinates that did not sit on the real streets, and two routes whose side
was impossible for the street's orientation (a north-south street has east
and west sides).

Reference data loads via `pnpm load-addresses`.
Contains information licensed under the Open Government Licence – Toronto.
AbeerDas added a commit that referenced this pull request Jul 29, 2026
The July review locked four changes that superseded earlier decisions, and
6e7b46f implemented all four plus GET /api/overview. Only design_decisions.md
was updated at the time; the spec, the data model, and the finance flow still
described the model we replaced. Those are what the design engineers read, so
the drift was live.

finances_flow.md needed the most work because it used "freeze" to mean what
closing does, in seven places including a state diagram — and the new model
makes freeze a separate per-cell state. The two are now distinguished
explicitly: closing detaches every cell in an issue from live calculation and
writes nothing, freezing snapshots one cell and is reversible, paying locks one
cell against everything. Adds Frozen to the calculation state machine with the
override -> frozen -> calculated precedence, a coverage state machine for
substitutes (3d), and the two new flows as 4j and 4k. Narrows 4g to genuine
reallocation and points coverage at substitutes instead.

Section letters 4a-4i are unchanged on purpose: eight service files cite
"finance flow 4c/4g/4h/4i" in comments, and renumbering would churn code that
was just reviewed for no behavioural gain.

api_spec.md gains the freeze/unfreeze/substitute actions, a section for
GET /api/overview with its full response type, the required startDate on year
creation, the amount precedence, and the 409 rules per action. data_model.md
gains frozenAmount/frozenAt/substituteCaptainId on CaptainPayout,
FinancialYear.startDate, and the corrected PayCadence enum.

House-count spec drift is deliberately untouched — #23 resolves it coherently
and fixing it here would only conflict.

open_items.md records that the numeric-as-string premise behind the coercion
helpers did not reproduce against the live project, which returns numeric as
JSON numbers. Their code is left exactly as merged for @kenzysoror to confirm.
AbeerDas added 2 commits July 29, 2026 18:03
Picks up the merged backend (#12) and the dependency resolution done one level
up, so this merge had no conflicts of its own.

Reformats lib/services/house-count.ts for the same Prettier rule change main
brought in — cosmetic, one union type re-wrapped.

Migrations interleave correctly: 20260728 (Toronto address points) before
20260729 (finance reconciliation), so no reordering was needed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant