Files
gods-eye-view/src/annotations/annotationResolver.test.mjs
T
Niccolò Saggese c5b1d41cba fix(search): geocode through the server, not the browser
- Google's Geocoding web service refuses referrer-restricted keys, so the
  browser call forced GOOGLE_MAPS_API_KEY to ship unrestricted
- add /api/google/geocode and /api/google/reverse-geocode, using the server
  key like Places and Street View already do
- the browser key now needs the Map Tiles API only
2026-09-21 11:30:49 +02:00

625 lines
27 KiB
JavaScript

import { createStandalonePlaceSearch } from '../standalone/placeSearch.js';
import { createPlaceSearch } from '../search/placeSearch.js';
// Footprint-selection contract tests — pure fixtures, no network, no browser.
//
// Locks the field-test-7 monument fix (docs/field-test-rootcause-2026-06-30.md §1):
// a POINT-LIKE target ("Tejano Monument, Austin") must never adopt a nearby
// polygon that merely shares locality/context words ("Austin", "History").
// The fixtures replicate the REAL Overpass candidates captured over the Texas
// Capitol on 2026-07-01, where "Thompson Austin" (a hotel 680 m away) outscored
// everything because `nameOverlap * 1000` paid +1000 for the single word
// "Austin" — the monument itself is an OSM node and never even a candidate.
//
// Run with: npm test (node --test)
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
resolveAnnotationTarget as resolveTarget,
selectFootprint,
refineScope,
isGroundsLikeAsk,
} from './annotationResolver.js';
// Build a square way of ~`areaM2` centred `dLatM`/`dLonM` metres from an anchor,
// in Overpass `out geom` element shape ({ tags, geometry: [{lat,lon}...] }).
function squareWay(anchor, dLatM, dLonM, areaM2, tags) {
const mLat = 111320;
const mLon = mLat * Math.cos((anchor.lat * Math.PI) / 180);
const cLat = anchor.lat + dLatM / mLat;
const cLon = anchor.lon + dLonM / mLon;
const half = Math.sqrt(areaM2) / 2;
const dy = half / mLat;
const dx = half / mLon;
const ring = [
{ lat: cLat - dy, lon: cLon - dx },
{ lat: cLat - dy, lon: cLon + dx },
{ lat: cLat + dy, lon: cLon + dx },
{ lat: cLat + dy, lon: cLon - dx },
{ lat: cLat - dy, lon: cLon - dx },
];
return { type: 'way', tags, geometry: ring };
}
// The real Places anchor for "Tejano Monument, Austin" (on the Capitol grounds).
const ANCHOR = { lat: 30.27297, lon: -97.74029 };
// Real captured wrong-winners: named downtown features that share ONLY "Austin".
const wrongWinners = () => [
squareWay(ANCHOR, -660, -90, 3093, { building: 'yes', tourism: 'hotel', name: 'Thompson Austin' }),
squareWay(ANCHOR, -780, 120, 3772, { building: 'yes', name: 'Austin Police HQ' }),
squareWay(ANCHOR, -380, -60, 4499, { leisure: 'park', name: 'Black Austin Matters' }),
squareWay(ANCHOR, -240, -700, 1500, { building: 'yes', amenity: 'library', name: 'Austin Public Library - Austin History Center' }),
// The real enclosing grounds polygon — unnamed overlap with the monument query.
squareWay(ANCHOR, 0, 0, 100_000, { leisure: 'park', name: 'Capitol Square' }),
];
test('point mode: locality-word polygons never stand in for a monument (the Tejano bug)', () => {
const fp = selectFootprint(wrongWinners(), ANCHOR.lat, ANCHOR.lon, 'Tejano Monument, Austin', 'point');
assert.equal(fp, null); // keep the honest point anchor
});
test('point mode: an (almost) exactly-named, monument-scale polygon still outlines', () => {
const els = wrongWinners();
els.push(squareWay(ANCHOR, 5, 5, 300, { tourism: 'artwork', name: 'Tejano Monument' }));
const fp = selectFootprint(els, ANCHOR.lat, ANCHOR.lon, 'Tejano Monument, Austin', 'point');
assert.ok(fp, 'expected the true monument way to be accepted');
assert.equal(fp.kind, 'area');
// Ring is centred on the true feature (a few metres from the anchor), not downtown.
const lat0 = fp.ring[0][1];
assert.ok(Math.abs(lat0 - ANCHOR.lat) < 0.001, `ring landed at ${lat0}, expected ~${ANCHOR.lat}`);
});
test('point mode: an exactly-named but park-sized polygon is rejected (size cap)', () => {
// "Pioneer Monument, Golden Gate Park" — the park matches 3/3 of its own name
// and contains the anchor, but a 4.1 km² park must not render as a monument.
const park = squareWay(ANCHOR, 0, 0, 4_100_000, { leisure: 'park', name: 'Golden Gate Park' });
const fp = selectFootprint([park], ANCHOR.lat, ANCHOR.lon, 'Pioneer Monument, Golden Gate Park', 'point');
assert.equal(fp, null);
});
test('loose mode: unchanged — word-overlap scoring still picks the named candidate', () => {
// Pins that the fix is SCOPED to point-like targets: generic loose lookups keep
// the existing scorer (changing it globally would need its own field evidence).
const fp = selectFootprint(wrongWinners(), ANCHOR.lat, ANCHOR.lon, 'Tejano Monument, Austin', 'loose');
assert.ok(fp, 'loose mode still resolves a footprint');
});
test('loose mode: a named compound still beats an unnamed containing building (Presidio case)', () => {
const els = [
squareWay(ANCHOR, 0, 0, 900, { building: 'yes' }), // unnamed building under the anchor
squareWay(ANCHOR, 400, 400, 6_000_000, { landuse: 'military', name: 'Presidio of San Francisco' }),
];
const fp = selectFootprint(els, ANCHOR.lat, ANCHOR.lon, 'Presidio of San Francisco', 'loose');
assert.ok(fp);
assert.equal(fp.kind, 'area');
});
test('strict mode: unchanged — named district-sized areas only', () => {
const els = [
squareWay(ANCHOR, 0, 0, 900, { building: 'yes', name: 'Mission Lofts' }), // building → rejected
squareWay(ANCHOR, 0, 0, 12_000, { landuse: 'retail', name: 'Mission Market' }), // tiny parcel → rejected
squareWay(ANCHOR, 200, 200, 500_000, { landuse: 'residential', name: 'Mission District' }),
];
const fp = selectFootprint(els, ANCHOR.lat, ANCHOR.lon, 'Mission District', 'strict');
assert.ok(fp);
assert.equal(fp.kind, 'area');
assert.ok(Math.abs(fp.ring[0][1] - (ANCHOR.lat + 200 / 111320)) < 0.01);
});
test('loose mode: a named water body beats a shore feature named after it (field test 9)', () => {
// "Lady Bird Lake" — the anchor sits ON the water. Before natural=water joined the
// sweep, the lake was never a candidate and a shoreline park NAMED AFTER it won on
// word overlap, drawing a squiggle on the bank instead of the lake.
const ANCHOR_ON_WATER = { lat: 30.2565, lon: -97.7365 };
const els = [
// The real lake: large named water polygon containing the anchor.
squareWay(ANCHOR_ON_WATER, 0, 0, 3_500_000, { natural: 'water', water: 'reservoir', name: 'Lady Bird Lake' }),
// Shore park named after the lake (partial name coverage, does not contain anchor).
squareWay(ANCHOR_ON_WATER, 450, -300, 90_000, { leisure: 'park', name: 'Auditorium Shores at Lady Bird Lake Metropolitan Park' }),
];
const fp = selectFootprint(els, ANCHOR_ON_WATER.lat, ANCHOR_ON_WATER.lon, 'Lady Bird Lake, Austin', 'loose');
assert.ok(fp);
assert.equal(fp.kind, 'area');
// Ring centred on the lake fixture, not offset onto the shore park.
const lat0 = fp.ring[0][1];
assert.ok(Math.abs(lat0 - ANCHOR_ON_WATER.lat) < 0.02, 'ring at ' + lat0);
assert.ok(Math.abs(lat0 - (ANCHOR_ON_WATER.lat + 450 / 111320)) > 0.001, 'must not be the shore park');
// Inverse: a LAND ask near the water is not stolen by the lake polygon.
const park = selectFootprint(els, ANCHOR_ON_WATER.lat + 0.004, ANCHOR_ON_WATER.lon - 0.003, 'Auditorium Shores', 'loose');
assert.ok(park);
assert.ok(Math.abs(park.ring[0][1] - (ANCHOR_ON_WATER.lat + 450 / 111320)) < 0.01, 'land ask keeps the park');
});
test('isGroundsLikeAsk: label wording and entityKind both count (field test 8)', () => {
// The model's real call shape: grounds word only in the LABEL, compound entityKind.
assert.equal(isGroundsLikeAsk('Texas State Capitol, Austin', 'Capitol grounds', 'compound'), true);
// Label alone is enough when no entityKind is given.
assert.equal(isGroundsLikeAsk('Texas State Capitol, Austin', 'Capitol grounds', null), true);
// Target wording still works as before.
assert.equal(isGroundsLikeAsk('Texas State Capitol grounds, Austin', null, null), true);
// An explicit non-compound entityKind vetoes grounds wording (trust the model's fact).
assert.equal(isGroundsLikeAsk('Capitol complex', 'the complex', 'building'), false);
// A plain building ask is not grounds-like.
assert.equal(isGroundsLikeAsk('Texas State Capitol, Austin', 'Texas State Capitol', null), false);
});
test('refineScope: entityKind refines only an unresolved (auto) scope', () => {
assert.equal(refineScope('auto', 'building'), 'building');
assert.equal(refineScope('auto', 'compound'), 'compound');
assert.equal(refineScope('auto', 'district'), 'neighborhood');
assert.equal(refineScope('auto', 'street'), 'street');
assert.equal(refineScope('auto', 'point_feature'), 'auto'); // point-first handled separately
assert.equal(refineScope('auto', undefined), 'auto');
// Real geocode types (data) always win over the model's claim.
assert.equal(refineScope('city', 'building'), 'city');
assert.equal(refineScope('neighborhood', 'street'), 'neighborhood');
});
function closeViewportViewer() {
return {
camera: {
positionCartographic: {
latitude: 30.2672 * Math.PI / 180,
longitude: -97.7431 * Math.PI / 180,
height: 1000,
},
},
};
}
function geocodePayload({ lat, lon, types, label }) {
return {
status: 'OK',
results: [{
formatted_address: label,
types,
address_components: [{ long_name: label.split(',')[0], types }],
geometry: { location: { lat, lng: lon } },
}],
};
}
function installGoogleMocks(t, handler) {
const originalWindow = globalThis.window;
const originalFetch = globalThis.fetch;
globalThis.window = {
__GOOGLE_MAPS_API_KEY__: 'unit-test-key',
setTimeout: globalThis.setTimeout,
clearTimeout: globalThis.clearTimeout,
};
globalThis.fetch = handler;
t.after(() => {
if (originalWindow === undefined) delete globalThis.window;
else globalThis.window = originalWindow;
globalThis.fetch = originalFetch;
});
}
test('ask-side admin bypass: "the Texas Capitol" recovers near-view despite a far state-typed geocode', async (t) => {
const calls = [];
installGoogleMocks(t, async (url) => {
calls.push(String(url));
if (String(url).startsWith('/api/google/geocode')) {
return { json: async () => geocodePayload({
lat: 31.0000,
lon: -99.0000,
types: ['administrative_area_level_1', 'political'],
label: 'Texas, USA',
}) };
}
assert.match(String(url), /^\/api\/google\/text-search\?/);
return {
ok: true,
json: async () => ({ places: [{
latitude: 30.2747,
longitude: -97.7404,
name: 'Texas Capitol',
types: ['premise'],
}] }),
};
});
const resolved = await resolveAnnotationTarget({
viewer: closeViewportViewer(),
target: 'the Texas Capitol',
});
assert.ok(resolved);
assert.equal(resolved.source, 'places');
assert.deepEqual([resolved.lat, resolved.lon], [30.2747, -97.7404]);
assert.equal(calls.length, 2, 'admin result types must not suppress near-view recovery');
});
test('ask-side admin bypass: explicit "state of Texas" skips recovery and proximity gating', async (t) => {
const calls = [];
installGoogleMocks(t, async (url) => {
calls.push(String(url));
assert.match(String(url), /^\/api\/google\/geocode\?/);
return { json: async () => geocodePayload({
lat: 31.0000,
lon: -99.0000,
types: ['administrative_area_level_1', 'political'],
label: 'Texas, USA',
}) };
});
const resolved = await resolveAnnotationTarget({
viewer: closeViewportViewer(),
target: 'state of Texas',
});
assert.ok(resolved, 'the explicit state ask keeps its legitimate far centroid');
assert.equal(resolved.source, 'geocode');
assert.deepEqual([resolved.lat, resolved.lon], [31, -99]);
assert.equal(calls.length, 1, 'explicit state scope bypasses near-view recovery');
});
for (const fixture of [
{
target: 'Empire State',
lat: 40.7484,
lon: -73.9857,
types: ['premise', 'tourist_attraction'],
},
{
target: 'Ohio State',
lat: 40.0067,
lon: -83.0305,
types: ['university'],
},
]) {
test(`ask-side admin bypass: trailing name "${fixture.target}" remains guarded`, async (t) => {
const calls = [];
installGoogleMocks(t, async (url) => {
calls.push(String(url));
if (String(url).startsWith('/api/google/geocode')) {
return { json: async () => geocodePayload({
lat: fixture.lat,
lon: fixture.lon,
types: fixture.types,
label: `${fixture.target}, USA`,
}) };
}
assert.match(String(url), /^\/api\/google\/text-search\?/);
return { ok: true, json: async () => ({ places: [] }) };
});
const resolved = await resolveAnnotationTarget({
viewer: closeViewportViewer(),
target: fixture.target,
});
assert.equal(resolved, null, 'a recovery miss continues through the proximity gate');
assert.equal(calls.length, 2, 'a proper name ending in State must try near-view recovery');
});
}
test('ask-side admin bypass: bare "Texas" remains on the guarded recovery path', async (t) => {
const calls = [];
installGoogleMocks(t, async (url) => {
calls.push(String(url));
if (String(url).startsWith('/api/google/geocode')) {
return { json: async () => geocodePayload({
lat: 31.0000,
lon: -99.0000,
types: ['administrative_area_level_1', 'political'],
label: 'Texas, USA',
}) };
}
assert.match(String(url), /^\/api\/google\/text-search\?/);
return { ok: true, json: async () => ({ places: [] }) };
});
const resolved = await resolveAnnotationTarget({
viewer: closeViewportViewer(),
target: 'Texas',
});
assert.equal(resolved, null, 'a recovery miss continues through the proximity gate');
assert.equal(calls.length, 2, 'bare state names are not an explicit admin-scoped ask');
});
test('ask-side admin bypass: admin level 2/3 result types never grant a township bypass', async (t) => {
const fixtures = new Map([
['FB-3 township level 2 fixture', ['administrative_area_level_2', 'locality', 'political']],
['FB-3 township level 3 fixture', ['administrative_area_level_3', 'locality', 'political']],
]);
const calls = [];
installGoogleMocks(t, async (url) => {
calls.push(String(url));
if (String(url).startsWith('/api/google/geocode')) {
const query = new URL(String(url)).searchParams.get('address');
return { json: async () => geocodePayload({
lat: 39.7817,
lon: -89.6501,
types: fixtures.get(query),
label: `${query}, Illinois`,
}) };
}
assert.match(String(url), /^\/api\/google\/text-search\?/);
return {
ok: true,
json: async () => ({ places: [{
latitude: 30.2680,
longitude: -97.7425,
name: 'Local township fixture',
types: ['locality'],
}] }),
};
});
for (const target of fixtures.keys()) {
const resolved = await resolveAnnotationTarget({
viewer: closeViewportViewer(),
target,
});
assert.ok(resolved);
assert.equal(resolved.source, 'places');
}
assert.equal(
calls.filter((url) => url.startsWith('/api/google/text-search')).length,
2,
'both township-level admin result types stay guarded',
);
});
// The Capitol geocoder response contains its coordinates but only ADDRESS names.
// Synthetic footprints reproduce the real Capitol/YOTEL selection without live APIs.
const CAPITOL_ANCHOR = { lat: 38.8899389, lon: -77.0090505 };
function capitolViewer() {
return { camera: { positionCartographic: {
latitude: CAPITOL_ANCHOR.lat * Math.PI / 180,
longitude: CAPITOL_ANCHOR.lon * Math.PI / 180,
height: 1000,
} } };
}
function installCapitolMocks(t, elements, components = [
{ long_name: 'Washington', types: ['locality', 'political'] },
{ long_name: 'Capitol Hill', types: ['neighborhood', 'political'] },
]) {
installGoogleMocks(t, async (url) => {
if (String(url).startsWith('/api/google/geocode')) {
return { json: async () => ({ status: 'OK', results: [{
formatted_address: 'Washington, DC 20004, USA',
types: ['establishment', 'landmark', 'point_of_interest', 'tourist_attraction'],
address_components: components,
geometry: { location: { lat: CAPITOL_ANCHOR.lat, lng: CAPITOL_ANCHOR.lon } },
}] }) };
}
assert.equal(String(url), '/api/overpass');
return { ok: true, status: 200, json: async () => ({ elements }) };
});
}
for (const [target, deferFootprint] of [
['United States Capitol', true],
['United States Capitol, Washington, DC', false],
]) {
test(`address-only geocode retains landmark identity: ${target}`, async t => {
const capitol = squareWay(CAPITOL_ANCHOR, 0, 0, 20000, { building: 'yes', name: 'United States Capitol' });
const hotel = squareWay(CAPITOL_ANCHOR, 620, -140, 3000, { building: 'yes', tourism: 'hotel', name: 'YOTEL Washington DC' });
installCapitolMocks(t, [hotel, capitol]);
const result = await resolveAnnotationTarget({ viewer: capitolViewer(), target, entityKind: 'building', footprint: true, deferFootprint });
assert.ok(result);
const outline = deferFootprint ? await result.resolveOutline() : result;
assert.deepEqual(outline.ring, capitol.geometry.map(p => [p.lon, p.lat]));
assert.equal(outline.footprintKind, 'building');
});
}
test('address-only landmark without a matching outline keeps its geocoded point', async t => {
const hotel = squareWay(CAPITOL_ANCHOR, 620, -140, 3000, { building: 'yes', tourism: 'hotel', name: 'YOTEL Washington DC' });
installCapitolMocks(t, [hotel]);
const result = await resolveAnnotationTarget({ viewer: capitolViewer(), target: 'US Capitol building, Washington', entityKind: 'building', footprint: true, deferFootprint: true });
assert.ok(result);
assert.deepEqual([result.lat, result.lon], [CAPITOL_ANCHOR.lat, CAPITOL_ANCHOR.lon]);
assert.equal(await result.resolveOutline(), null);
});
test('missing address components do not turn a formatted city address into the landmark name', async t => {
const capitol = squareWay(CAPITOL_ANCHOR, 0, 0, 20000, { building: 'yes', name: 'United States Capitol' });
const hotel = squareWay(CAPITOL_ANCHOR, 620, -140, 3000, { building: 'yes', name: 'YOTEL Washington DC' });
installCapitolMocks(t, [hotel, capitol], []);
const result = await resolveAnnotationTarget({ viewer: capitolViewer(), target: 'United States Capitol building', footprint: true });
assert.deepEqual(result.ring, capitol.geometry.map(p => [p.lon, p.lat]));
});
test('a genuine geocoded feature name still canonicalizes an alternate user name', async t => {
const capitol = squareWay(CAPITOL_ANCHOR, 0, 0, 20000, { building: 'yes', name: 'United States Capitol' });
const decoy = squareWay(CAPITOL_ANCHOR, 620, -140, 3000, { building: 'yes', name: 'Congress meeting building' });
installCapitolMocks(t, [decoy, capitol], [{ long_name: 'United States Capitol', types: ['landmark'] }]);
const result = await resolveAnnotationTarget({ viewer: capitolViewer(), target: 'Congress meeting building', footprint: true });
assert.deepEqual(result.ring, capitol.geometry.map(p => [p.lon, p.lat]));
});
// ── Keyless geocoding ────────────────────────────────────────────────────────
//
// `geocodePlace` was Google-only: no key returned null, and so did a key whose
// Geocoding API is not enabled (Google answers HTTP 200 with REQUEST_DENIED).
// Either way the annotation silently failed to place. Both now fall through to
// Photon. These drive the second case, because it reaches the SAME fallback
// through a running Google branch — the no-key branch cannot be driven from
// `node --test`, since the key expression reads `import.meta.env`, which only
// Vite defines.
/** Photon's GeoJSON shape, trimmed to the properties the adapter consumes. */
function photonFeature({ name, lat, lon, tags = {}, extent = null, ...rest }) {
return {
geometry: { type: 'Point', coordinates: [lon, lat] },
properties: { name, ...tags, ...rest, ...(extent ? { extent } : {}) },
};
}
const GOOGLE_FOUND_NOTHING = { ok: true, json: async () => ({ status: 'ZERO_RESULTS', results: [] }) };
test('keyless: a key that geocodes to nothing still anchors the annotation', async (t) => {
const requests = [];
installGoogleMocks(t, async (url) => {
requests.push(String(url));
if (String(url).startsWith('/api/google/geocode')) return GOOGLE_FOUND_NOTHING;
return {
ok: true,
json: async () => ({
features: [photonFeature({
name: 'Lady Bird Lake',
lat: 30.25,
lon: -97.73,
city: 'Austin',
state: 'Texas',
country: 'United States',
tags: { osm_key: 'natural', osm_value: 'water' },
// Photon orders extent [west, north, east, south] — a naive [w,s,e,n]
// read would invert the box and still look like a valid viewport.
extent: [-97.8, 30.28, -97.68, 30.24],
})],
}),
};
});
const resolved = await resolveAnnotationTarget({
viewer: closeViewportViewer(),
target: 'Lady Bird Lake',
});
assert.ok(resolved, 'a Google miss must not leave the annotation unplaced');
assert.equal(resolved.source, 'geocode');
assert.deepEqual([resolved.lat, resolved.lon], [30.25, -97.73]);
// The label is shortened the same way a Google `formatted_address` is.
assert.equal(resolved.label, 'Lady Bird Lake');
// Photon's extent reaches the framing code in the Places `{low,high}` shape,
// right way up — this is what sizes a grounds disc and the flyTo box.
assert.deepEqual(resolved.viewport, {
low: { latitude: 30.24, longitude: -97.8 },
high: { latitude: 30.28, longitude: -97.68 },
});
// Google is asked first and exactly once; one Photon call answers it.
assert.equal(requests.filter((url) => url.includes('/api/google/geocode')).length, 1);
assert.equal(requests.filter((url) => url.includes('photon.komoot.io')).length, 1);
});
test('keyless: OSM matching uses the feature\'s canonical name, not the user\'s words', async (t) => {
// The reason `primaryName` is carried out of Photon at all. The ask names a
// landmark AND its surroundings; the geocoded feature is called just "Tejano
// Monument". Score the OSM candidates on the raw utterance and the locality
// tokens win — this is the Thompson-Austin bug (field test 7) reached through
// the keyless path. Score them on the canonical name and the monument wins.
const decoy = squareWay(ANCHOR, -250, -250, 3000, {
building: 'yes', tourism: 'hotel', name: 'Texas Capitol Austin Visitor Center',
});
const monument = squareWay(ANCHOR, 5, 5, 300, { tourism: 'artwork', name: 'Tejano Monument' });
installGoogleMocks(t, async (url, init) => {
const href = String(url);
if (href.startsWith('/api/google/geocode')) return GOOGLE_FOUND_NOTHING;
if (href.startsWith('/api/google/text-search')) return { ok: true, json: async () => ({ places: [] }) };
if (href.startsWith('https://photon.komoot.io/')) {
return {
ok: true,
json: async () => ({
features: [photonFeature({
name: 'Tejano Monument',
lat: ANCHOR.lat,
lon: ANCHOR.lon,
city: 'Austin',
tags: { osm_key: 'historic', osm_value: 'memorial' },
})],
}),
};
}
assert.equal(href, '/api/overpass');
assert.equal(init?.method, 'POST');
return { ok: true, status: 200, headers: { get: () => null }, json: async () => ({ elements: [decoy, monument] }) };
});
const resolved = await resolveAnnotationTarget({
viewer: closeViewportViewer(),
target: 'the monument near the Texas Capitol, Austin',
footprint: true,
});
assert.ok(resolved.ring, 'the monument polygon is monument-scale and exactly named — it must outline');
// The anchor re-centres on the chosen polygon, so its position IS the verdict:
// the monument sits on the anchor, the decoy 354 m south-west of it.
assert.ok(
approximateMetres(resolved.lat, resolved.lon, ANCHOR.lat, ANCHOR.lon) < 50,
`outlined a polygon ${Math.round(approximateMetres(resolved.lat, resolved.lon, ANCHOR.lat, ANCHOR.lon))} m from the monument`,
);
});
/** Metres between two coordinates — enough precision for a few hundred metres. */
function approximateMetres(lat1, lon1, lat2, lon2) {
const mLat = 111320;
const dy = (lat1 - lat2) * mLat;
const dx = (lon1 - lon2) * mLat * Math.cos((lat1 * Math.PI) / 180);
return Math.hypot(dx, dy);
}
test('keyless: a Photon outage is not remembered as "no such place"', async (t) => {
// Google answering ZERO_RESULTS is a verdict about GOOGLE. Photon holds
// plenty of places Google does not — that asymmetry is why this fallback
// exists — so a Google verdict plus a Photon timeout must leave the query
// open. Caching it would keep an annotation unplaceable for the rest of the
// session, on a network that has since recovered.
const photonUp = {
ok: true,
json: async () => ({
features: [photonFeature({
name: 'Waller Creek', lat: 30.2665, lon: -97.7385, city: 'Austin',
tags: { osm_key: 'waterway', osm_value: 'stream' },
})],
}),
};
let photonReachable = false;
const requests = [];
installGoogleMocks(t, async (url) => {
requests.push(String(url));
if (String(url).startsWith('/api/google/geocode')) return GOOGLE_FOUND_NOTHING;
if (!photonReachable) throw new Error('network down');
return photonUp;
});
const target = 'Waller Creek';
assert.equal(await resolveAnnotationTarget({ viewer: closeViewportViewer(), target }), null);
const duringOutage = requests.length;
photonReachable = true;
const retried = await resolveAnnotationTarget({ viewer: closeViewportViewer(), target });
assert.ok(retried, 'the same target must resolve once the network is back');
assert.deepEqual([retried.lat, retried.lon], [30.2665, -97.7385]);
assert.ok(
requests.length > duringOutage,
'the retry was served from a poisoned cache instead of asking again',
);
});
test('a not-found is remembered only when every source consulted gave a verdict', async () => {
// The full truth table, because the no-key row is the one the behavioural
// tests above cannot reach: `import.meta.env` only exists under Vite, so
// `node --test` can never take the branch that skips Google entirely.
const cases = [
// no key: Photon is the only source, so Photon alone decides
[{ googleConsulted: false, photonAnswered: true }, true],
[{ googleConsulted: false, photonAnswered: false }, false],
// with a key: Google must have said "no such place", AND Photon must have replied
[{ googleConsulted: true, googleSaysNoSuchPlace: true, photonAnswered: true }, true],
[{ googleConsulted: true, googleSaysNoSuchPlace: true, photonAnswered: false }, false],
// Google declined (REQUEST_DENIED / OVER_QUERY_LIMIT) — never a verdict
[{ googleConsulted: true, googleSaysNoSuchPlace: false, photonAnswered: true }, false],
[{ googleConsulted: true, googleSaysNoSuchPlace: false, photonAnswered: false }, false],
];
for (const [sources, expected] of cases) {
const providers = [sources.googleConsulted ? Boolean(sources.googleSaysNoSuchPlace) : true, sources.photonAnswered]
.map((answered) => ({ geocode: async () => ({ place: null, answered }) }));
assert.equal((await createPlaceSearch({ providers }).geocode('miss')).answered, expected, JSON.stringify(sources));
}
// An omitted Google verdict defaults to "did not say no such place".
});
function resolveAnnotationTarget(options) {
return resolveTarget({ placeSearch: createStandalonePlaceSearch({ resolveApiKey: () => globalThis.window?.__GOOGLE_MAPS_API_KEY__ }), ...options });
}