CLIM-1322 (3): Popup Title Display Discrepancy When InteractiveRegionOption.GRIDDED_DATA#684
CLIM-1322 (3): Popup Title Display Discrepancy When InteractiveRegionOption.GRIDDED_DATA#684renoirb-crim wants to merge 8 commits into
Conversation
geocoder.all_areas has 648 distinct gen_term values and no categorization
column, so cdc_get_location_by_coords() and cdc_location_search() were
playing whack-a-mole with string filter lists that drifted apart over
time. Replace both functions' filtering with a shared tiered-preference
table (City/Town/Township > Village/Municipality/Hamlet > Borough; rest
falls to tier 99) plus a single hard-exclusion list, both living in
fw-child/resources/functions/gen-term-preferences.php so any future
adjustment is a one-file edit consumed by both endpoints.
cdc_get_location_by_coords() drops the preferred_terms two-pass and the
progressive-widening loop ($ranges = [0.05, 0.1, 0.2]) in favour of a
single bounding-box query (knob: \$bbox_range = 0.5, commented at the
call site) ordered by preference_tier ASC, distance ASC LIMIT 1. Folds
in CLIM-1322_2_'s Atom 1 (province_fr aliased AS province) so the same
fix lives in one place across branches.
Both functions now use mysqli_prepare + bind_param + stmt_get_result;
\$_GET['lat'], \$_GET['lng'], and \$_GET['q'] are bound rather than
interpolated. Response shapes preserved: location_search keeps {id,
text, term, location, province, lat, lon}; get_location_by_coords keeps
{geo_id, geo_name, generic_term, location, province, lat, lon, lng,
distance, coords, province_short, title} for parity with sibling Atom
1+2 in CLIM-1322_2_.
…Township to tier 2
Probes after Atom 10 surfaced two principled-but-undesired results: Toronto
resolved to York (Geographic Township) instead of Toronto (City), and Halifax
fell to Stewiacke because Metropolitan Area was absent from the tier list and
got dumped into the Tier 99 fallback. Re-tiers Township-class down to Tier 2,
and reinstates the legacy two-pass preferred_terms ('Metropolitan Area',
'Community') in Tier 1 so metro-class places (Halifax, Vancouver, Edmonton)
resolve correctly.
…alone for metros Atom 11 promoted Community to Tier 1 alongside Metropolitan Area to fix the Halifax-class metro case, but Community is double-purpose in geocoder.all_areas — it serves both as a canonical city name (e.g. Toronto downtown) AND as neighborhood names within larger cities. The neighborhood usage regressed Vancouver to "West End, BC" (Community at 919m) beating "Vancouver, BC" (City at 2.5km) — exactly the original Carrington-class wrong-name pathology this ticket is fixing. Metropolitan Area alone in Tier 1 keeps Halifax fixed without the Community bleed; Community now falls to the implicit Tier 99 fallback and only wins when no Tier-1/2/3 row exists in the bounding box. Probe matrix — all 10 anchors return the canonical city/town: ┌─────────────────────────┬──────────────────┬─────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Anchor │ Returned │ tier 1 generic_term │ distance │ ├─────────────────────────┼──────────────────┼─────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ Vancouver │ Vancouver, BC │ City │ 2.5 km ✅ │ ├─────────────────────────┼──────────────────┼─────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ McMasterville │ Beloeil, QC │ Ville │ 3.2 km ✅ (closest Town per spec — UC-Search uses autocomplete row directly via Atom 9-equiv anyway) │ ├─────────────────────────┼──────────────────┼─────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ Bedford │ Bedford, QC │ Town │ 0 ✅ │ ├─────────────────────────┼──────────────────┼─────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ Sainte-Adèle │ Sainte-Adèle, QC │ Ville │ 0 ✅ │ ├─────────────────────────┼──────────────────┼─────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ Montréal downtown │ Montréal, QC │ Town │ 1.3 km ✅ │ ├─────────────────────────┼──────────────────┼─────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ Edmonton │ Edmonton, AB │ City │ 1.3 km ✅ │ ├─────────────────────────┼──────────────────┼─────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ Calgary │ Calgary, AB │ City │ 1.0 km ✅ │ └─────────────────────────┴──────────────────┴─────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────┘
…e province_short in autocomplete
Atom 9-equivalent's buildLocationTitle() produced the verbose autocomplete
dropdown display ("Montréal, (Ville), Montréal; Montréal, Québec") in the
popup title; make UC-Search's popup match the server-resolved short shape
("Montréal, QC") that cdc_get_location_by_coords already returns.
Server-side cdc_location_search() now adds province_short to each item,
derived via the existing short_province() helper from fw-child/functions.php
(already used by cdc_get_location_by_coords and cdc_get_location_by_id), so
the client doesn't need its own province-name mapping. buildLocationTitle()
is unchanged — leaflet-search still uses it as the dropdown display key.
…iants stay in tier 2 Clicking near Verchères returned L'Assomption because Tier-1 Town beat Tier-2 Municipality regardless of distance. In Quebec — and broadly across Canada — Ville and Municipalité are equivalent municipal types: both are populated places governed by an elected council. Promote the generic 'Municipality' to Tier 1; keep specialized variants (Specialized / District / Rural / Parish / Resort / Mountain Resort / Township / United Townships / Village / Northern Village / Cree Village / Naskapi Village Municipality) in Tier 2 since they signal special-purpose context (parish = legacy religious admin, rural = prairie units, resort = tourism zoning) that shouldn't outrank a generic Municipality or Town nearby.
SQL query analysisBetween branches This is shared analysis context for review. The branches differ in how the two REST endpoints rank rows from Endpoint ↔ PHP function mapping
Route registrations (verbatim)
add_action ( 'rest_api_init', function () {
register_rest_route ( 'cdc/v2', '/get_location_by_coords/', array (
'methods' => 'GET',
'callback' => 'cdc_get_location_by_coords'
) );
});
add_action ( 'rest_api_init', function () {
register_rest_route ( 'cdc/v2', '/location_search/', array (
'methods' => 'GET',
'callback' => 'cdc_location_search'
) );
});1.
|
| Aspect | main | CLIM-1322_2 | CLIM-1322_3 |
|---|---|---|---|
| cdc_get_location_by_coords: passes | 3 queries (ranges [0.05, 0.1, 0.2]) | 1 query (0.2°) | 1 query (0.5°) |
| Filter style | Post-filter in PHP (in_array) | Hard exclusion (25 terms) | Hard exclusion (24 terms) |
| Ranking | in_array match on ['Community', 'Metropolitan Area'], fallback to distance | distance ASC | preference_tier ASC, distance ASC |
| Prepared statement | ✗ (string concat) | ✗ (string concat) | ✓ |
| Returns province_short | ✓ | ✓ | ✓ |
| Tier-aware | ✗ (hard-coded list) | ✗ | ✓ (Tiers 1–3 + Tier 99 implicit) |
| cdc_location_search: exclusion set | 3 terms | 3 terms | 24 terms (harmonized with coords) |
| Ranking | scale DESC | scale DESC | preference_tier ASC, scale DESC |
| Prepared statement | ✗ | ✗ | ✓ |
| Tier-aware | ✗ | ✗ | ✓ |
4. Behavioral consequences — why _2 and _3 produce different popup titles
The Saint-Anthony-of-Padua (Montreal centroid) case:
- Click at 45.5°N, 73.5°W (Montreal city center).
- Within 0.5° bbox:
- Town "Outremont" at ~900m.
- Public Park "Mount Royal" at ~1.5km.
- Borough "Saint-Louis" (arrondissement) at ~2km.
Pass 2 behavior (distance-first):
- Nearest wins: Outremont (Town, ~900m) returned.
- Parks are excluded, so Mount Royal doesn't enter.
- ✓ Correct for this case.
But with Public Parks in an excluded list (NOT IN), the system is fragile:
- T28 vulnerability: An unknown park at 800m beats a known Town at 900m if the park's gen_term is not in the exclusion list.
- The exclusion list (Pass 2) has 25 hardcoded terms; new gen_term values (e.g., from data updates) can slip through.
- Example: If geocoder adds "Nature Preserve" as a gen_term, it passes the exclusion check and beats any Town by distance alone.
Pass 3 behavior (tier-first):
- Nearest wins within each tier: Tier 1 (Town, Outremont) returned.
- Mount Royal (Tier 99, implicit fallback — park gen_term not in Tiers 1–3) is excluded from the final query because it's in the hard exclusion list (
gen_term NOT IN (...)). - But even if a park were in-scope and Tier 99, Outremont (Tier 1) would win regardless of distance.
- ✓ Correct, and resilient to data updates: any new gen_term falls into Tier 99, so Tier 1–3 always win.
Key difference:
- Pass 2: Distance-first strategy with a hard-coded blacklist → vulnerable to unknown gen_terms.
- Pass 3: Tier-first strategy with a soft preference system → unknown gen_terms default to Tier 99 and lose to Tier 1–3 by default.
5. Use-case scenarios — what each Pass does for each endpoint
| Use case | Path | main | CLIM-1322_2 | CLIM-1322_3 |
|---|---|---|---|---|
| UC-Search (autocomplete) | location_search | scale DESC | scale DESC | preference_tier ASC, then scale DESC |
| Behavior | Larger places first, no tier awareness. | Larger places first, no tier awareness. | Tier 1 (City/Town) always appears before Tier 2 (Village), even if smaller. | |
| UC-LocateMe (browser geoloc) | get_location_by_coords | 3 queries, preferred_terms in PHP | 1 query, distance-only ranking | 1 query, tier-first ranking |
| Behavior | Community/Metro in preferred_terms; fallback to nearest. | Nearest non-admin place. | Nearest Tier 1 place (City/Town); Tier 2 as fallback. | |
| UC-MapClick (map interaction) | get_location_by_coords | Same as LocateMe | Same as LocateMe | Same as LocateMe |
| UC-RawCoordPaste (manual coords) | get_location_by_coords | Same as LocateMe | Same as LocateMe | Same as LocateMe |
| UC-PolygonCENSUS (Shapefile mode) | Not affected | — | — | — |
| UC-PolygonHEALTH (Health region) | Not affected | — | — | — |
| UC-PolygonWATERSHED (Watershed) | Not affected | — | — | — |
Notes:
- All polygon modes (CENSUS, HEALTH, WATERSHED) are unchanged across passes.
- The ranking changes only affect the fallback nearest-place name when no polygon is selected.
- Pass 3 search and location-by-coords are aligned in exclusion and tier rules (CLIM-1322 requirement) — they now return the same name for the same place.
Appendix: Tier definitions (from gen-term-preferences.php)
Tier 1 (5 terms — major settlements with population centers):
- City, Town, Separated Town, Metropolitan Area, Municipality (generic)
Tier 2 (30 terms — smaller settlements and special-purpose municipalities):
- Township, Township Municipality, Geographic Township, United Townships Municipality
- Village, Village Municipality, Northern Village, Northern Village Municipality
- Resort Village, Summer Village, Rural Village, Police Village, Forest Village, Provincial Historic Village
- Cree Village, Cree Village Municipality, Naskapi Village, Naskapi Village Municipality
- First Nation Village, Former First Nation Village
- Specialized Municipality, District Municipality, Rural Municipality, Parish Municipality
- Resort Municipality, Mountain Resort Municipality
- Hamlet, Northern Hamlet, Organized Hamlet
- Townsite
Tier 3 (1 term — borough / arrondissement):
- Borough
Implicit Tier 99 (fallback — anything not in Tiers 1–3):
- Community (neighborhoods within cities, not canonical places)
- Parks, lakes, cemeteries, points-of-interest
- Any new gen_term values from data updates
Hard exclusions (24 terms — never returned, regardless of tier):
- Administrative Region, Province, Territory
- Census Division, Census Subdivision
- Regional Municipality, County Regional Municipality, County Municipality, Municipal County, Municipal District
- Region, Regional District, Restructured County, County
- Improvement District, Local Government District, Local Service District, Local Urban District
- Subdivision, Unorganized Territory
- Urban Community, Administrative Sector
- Railway Point, Railway Junction
Analysis complete. Prepared for colleague review.
WHY: Renoir reading the data-driven SQL-fragment helper found the placeholder/bind-type/value arrays hard to follow on first read. The existing helper docblock described outputs abstractly without showing the input -> output shape with concrete values, and the call-site array_merge sequences had no reminder that order is positionally tied to $types. Add a worked-example block to the helper docblock (showing actual gen_term strings flowing into case_values, case_types, etc), one inline comment in the foreach loop explaining what each iteration emits, and a two-line "order MUST mirror $types" reminder above each array_merge call site in rest.php. No logic changes.
Edited: 2026-05-19 This branch was originally written BEFORE creating `dispatchMapClick` and has been now merged to main in PR #676. During the rebase that followed 676 the important aspect of `CLIM-1322_Popup-Title-Name-Resolution` was to pass the title. Passing the title was an illustrative "band aid" and we've kept this branch to return back to it later. After Passes 2 supplemental passes (...): - #682 branch `CLIM-1322_2_Popup-Title-Name-Resolution` - #684 branch `CLIM-1322_3_Popup-Title-Name-Resolution` We've decided to return to what this branch initially suggested. Simply to pass the title from the click handler. This commit is there to illustrate what was lost during the iterations to set the record straight. Commits that had gotten lost and were part of this branch this commit is replacing: - 72a0df4 - f9169bb Earlier commit message: Map search popup was showing neighboring Community names (e.g. "Toronto" → "North York") because handleLocationChange fired a synthetic click that discarded the search item's pre-resolved title, forcing handleClick to re-derive from lat/lng via fetchLocationByCoords, which snaps to the nearest 'Community' / 'Metropolitan Area' and filters out 'City' entries server-side. D1a from the Analysis Plan: plumb buildLocationTitle(item) through handleLocationChange into the synthetic click payload as an optional searchProvidedTitle field. handleClick prefers it over fetchLocationByCoords when the mode is GRIDDED_DATA, skipping the network call on the happy path. This is the third optional field on the synthetic click payload (joining layer.properties from CLIM-1223). Submitted as a draft PR for team discussion on whether to continue the bandaid pattern or escalate to the structural refactor in D1c. See [[LLM-Context-ClimateData-Ticket-CLIM-1322-Analysis-Plan]] for the full design trade-off.
Edited: 2026-05-19 This branch was originally written BEFORE creating `dispatchMapClick` and has been now merged to main in PR #676. During the rebase that followed 676 the important aspect of `CLIM-1322_Popup-Title-Name-Resolution` was to pass the title. Passing the title was an illustrative "band aid" and we've kept this branch to return back to it later. After Passes 2 supplemental passes (...): - #682 branch `CLIM-1322_2_Popup-Title-Name-Resolution` - #684 branch `CLIM-1322_3_Popup-Title-Name-Resolution` We've decided to return to what this branch initially suggested. Simply to pass the title from the click handler. This commit is there to illustrate what was lost during the iterations to set the record straight. Commits that had gotten lost and were part of this branch this commit is replacing: - 72a0df4 - f9169bb Earlier commit message: Map search popup was showing neighboring Community names (e.g. "Toronto" → "North York") because handleLocationChange fired a synthetic click that discarded the search item's pre-resolved title, forcing handleClick to re-derive from lat/lng via fetchLocationByCoords, which snaps to the nearest 'Community' / 'Metropolitan Area' and filters out 'City' entries server-side. D1a from the Analysis Plan: plumb buildLocationTitle(item) through handleLocationChange into the synthetic click payload as an optional searchProvidedTitle field. handleClick prefers it over fetchLocationByCoords when the mode is GRIDDED_DATA, skipping the network call on the happy path. This is the third optional field on the synthetic click payload (joining layer.properties from CLIM-1223). Submitted as a draft PR for team discussion on whether to continue the bandaid pattern or escalate to the structural refactor in D1c. See [[LLM-Context-ClimateData-Ticket-CLIM-1322-Analysis-Plan]] for the full design trade-off.
Popup Title Display Discrepancy When InteractiveRegionOption.GRIDDED_DATA
Related:
WIP
Description
Related ticket