A tester's cached-contacts map came up empty and Path Analyzer drew routes
across the globe. Two of his 2131 cached contacts carried garbage positions
from corrupted adverts - lat 1642 / lon -323 and lat 309 / lon -1768.
Leaflet clamps latitude when projecting but not longitude, so fitBounds()
spanned 1797 degrees, dropped to zoom 0 and centred on lon -870; all 2273
real markers were rendered several world-widths outside the viewport.
Reproduced against his DB: zoom 0, bounds -1431..-309. The device-only view
was fine because both bad rows are cache-only.
Four layers, so no single miss can blank a map again:
- app/geo.py: one definition of a usable coordinate, applied on write in
Database.upsert_contact() so the cache stops storing garbage, and on API
output so a cache poisoned by an older build heals itself with no
migration. Corrupted advert types (61, 126, 152, 210 seen in the wild)
are logged but kept raw - they already render as UNKNOWN.
- static/js/geo-utils.js: hasValidGps()/withValidGps()/fitMapToPoints()
replace eight copies of the same check across six files. Path Analyzer
had a looser variant that also accepted lon == 0, which put nodes off
west Africa and stretched routes across the map; it now shares the rule.
- Per-marker try/catch and a bounds validity check, so an entry that still
slips through costs its own pin rather than the whole map.
Verified against the tester's DB in a container: cached map 2273 markers,
lon span 1797 -> 19 degrees, centred on Poland; the route that drew the
world-spanning line (msg 29499, token 72AF) now renders as a real
Krakow-Kielce path with the bad hop left unresolved. Live local instance
regression-checked on all five map pages. tests/test_geo.py: 43 passed;
full suite 290 passed, 2 pre-existing failures unrelated to this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>