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 ───────────────────────────────────────────────────