From 63a57b92695c3f6317a5f2d284e2f301f9c88ae7 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 28 Aug 2026 19:37:37 +0200 Subject: [PATCH] fix(atlas): stop counting address abbreviations as countries The Atlas hero counted more countries than the continent bars below it drew, and more than the dashboard widget. The extra ones were invented: a place without coordinates fell back to parsing its address, and any two-letter uppercase segment at the end became a country code. "New York, NY" produced NY, "Toronto, ON" produced ON. Those phantoms only ever showed up in the one number. The continent strip files an unknown code under 'Other' and never draws it, the map skips codes it cannot resolve, and the dashboard builds its set from stored regions instead, which is why it stayed right. Filtering against a list of valid codes would not have been enough, and would have hidden the worse half of this: 26 US state abbreviations are real ISO country codes. CA is California far more often than Canada, DE is Delaware, IN is Indiana. The existing comment above resolveCountryCode already describes that collision and solves it by trusting coordinates first and sanity-checking the address against the country's own bounding box. The gap it names in its last line is this bug: with no coordinates there is nothing to check against. So the bare-code branch is now only offered to callers that have coordinates. A spelled-out country name still resolves either way. A lone "NY" or "CA" resolves to nothing, which is the honest answer. Fixes #2111 --- server/src/nest/atlas/atlas-geo.ts | 22 +++++++++++++++----- server/tests/unit/nest/atlas.service.test.ts | 14 +++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/server/src/nest/atlas/atlas-geo.ts b/server/src/nest/atlas/atlas-geo.ts index 3858b7ea7..aae5efbb1 100644 --- a/server/src/nest/atlas/atlas-geo.ts +++ b/server/src/nest/atlas/atlas-geo.ts @@ -725,7 +725,17 @@ export function getCountryFromCoords(lat: number, lng: number): string | null { return looseBoxFallback ?? candidates[0].code; } -export function getCountryFromAddress(address: string | null): string | null { +/** + * `allowBareCode` decides whether a trailing two-letter uppercase segment counts as + * a country. It only does when the caller can sanity-check the answer against + * coordinates, because that segment is far more often a state or province than a + * country: "…, New York, NY", "…, Toronto, ON". Half of those abbreviations are + * real ISO codes as well (CA, DE, LA, IN, MD, GA, PA, VA), so a list of valid + * country codes does not separate them, and the other half are codes for nothing + * at all, which used to inflate the Atlas country count with places that could + * never appear on the map or in the continent bars (#2111). + */ +export function getCountryFromAddress(address: string | null, allowBareCode = true): string | null { if (!address) return null; const parts = address .split(',') @@ -736,7 +746,7 @@ export function getCountryFromAddress(address: string | null): string | null { const normalized = last.toLowerCase(); if (NAME_TO_CODE[normalized]) return NAME_TO_CODE[normalized]; if (NAME_TO_CODE[last]) return NAME_TO_CODE[last]; - if (last.length === 2 && last === last.toUpperCase()) return last; + if (allowBareCode && last.length === 2 && last === last.toUpperCase()) return last; return null; } @@ -752,14 +762,16 @@ export function getCountryFromAddress(address: string | null): string | null { // to any country, the address result is sanity-gated against that country's own admin0 // bounding box (isPointInCountryBox) before being trusted — the same guard the region-level // address fallback uses. A place with no coordinates at all has nothing to gate against, so -// the address is trusted directly there, as before. +// it does not get the bare-code branch at all (#2111): a spelled-out country name still +// resolves there, a lone "NY" or "CA" no longer does. Guessing produced both phantom +// countries that nothing downstream could draw and confidently wrong ones. async function resolveCountryCode(place: Place): Promise { const hasCoords = !!(place.lat && place.lng); if (hasCoords) { const fromCoords = getCountryFromCoords(place.lat!, place.lng!); if (fromCoords) return fromCoords; } - const fromAddress = getCountryFromAddress(place.address); + const fromAddress = getCountryFromAddress(place.address, hasCoords); if (fromAddress && (!hasCoords || isPointInCountryBox(fromAddress, place.lat!, place.lng!))) { return fromAddress; } @@ -775,7 +787,7 @@ export function resolveCountryCodeSync(place: Place): string | null { const fromCoords = getCountryFromCoords(place.lat!, place.lng!); if (fromCoords) return fromCoords; } - const fromAddress = getCountryFromAddress(place.address); + const fromAddress = getCountryFromAddress(place.address, hasCoords); if (fromAddress && (!hasCoords || isPointInCountryBox(fromAddress, place.lat!, place.lng!))) { return fromAddress; } diff --git a/server/tests/unit/nest/atlas.service.test.ts b/server/tests/unit/nest/atlas.service.test.ts index 438fb2188..754ebcc0c 100644 --- a/server/tests/unit/nest/atlas.service.test.ts +++ b/server/tests/unit/nest/atlas.service.test.ts @@ -604,6 +604,20 @@ describe('getCountryFromAddress', () => { it('ATLAS-SVC-012: returns null for unrecognized country name', () => { expect(getCountryFromAddress('Unknown City, Unknown Country')).toBeNull(); }); + + // #2111 — the bare-code branch is a guess, and it is only allowed where the caller + // can check it against coordinates. + it('ATLAS-SVC-046: a bare trailing code is refused when the caller cannot verify it', () => { + expect(getCountryFromAddress('11 W 53rd St, New York, NY', false)).toBeNull(); + // Half of these abbreviations are real ISO codes, so a list of valid country + // codes would not have caught them: CA is California here, not Canada. + expect(getCountryFromAddress('1 Market St, San Francisco, CA', false)).toBeNull(); + expect(getCountryFromAddress('100 King St, Toronto, ON', false)).toBeNull(); + }); + + it('ATLAS-SVC-047: a spelled-out country still resolves without coordinates', () => { + expect(getCountryFromAddress('Eiffel Tower, Paris, France', false)).toBe('FR'); + }); }); // ── reverseGeocodeCountry ───────────────────────────────────────────────────