mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
fix: preserve landmark identity when resolving annotation outlines
This commit is contained in:
1 parent
f883dfbc1e
commit
38b591863c
4 files changed
+98
-7
No files matched your search
@@ -13,6 +13,10 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Keep landmark names when geocoding returns only address components, preventing
|
||||||
|
the United States Capitol annotation from moving to a Washington hotel.
|
||||||
|
Unrelated outlines leave the valid geocoded marker in place.
|
||||||
|
|
||||||
- Split aircraft and vessel server providers into focused modules for source
|
- Split aircraft and vessel server providers into focused modules for source
|
||||||
fetching, AIS records/tracks and shared request helpers; preserve existing
|
fetching, AIS records/tracks and shared request helpers; preserve existing
|
||||||
routes, local setup, fallback behavior and rendering.
|
routes, local setup, fallback behavior and rendering.
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
# God's Eye View Current State
|
# God's Eye View Current State
|
||||||
|
|
||||||
|
## Landmark annotation identity
|
||||||
|
|
||||||
|
When a landmark geocode contains only address components, annotations retain
|
||||||
|
its requested name for outline matching. A city or neighborhood address no
|
||||||
|
longer replaces the landmark's identity. Without a canonical feature name,
|
||||||
|
outline candidates must contain the geocoded anchor or closely match the
|
||||||
|
requested name; otherwise the annotation stays at its geocoded point.
|
||||||
|
Genuine feature-name components and existing administrative/monument matching
|
||||||
|
retain their established behavior.
|
||||||
|
|
||||||
## Build configuration and local provider boundaries
|
## Build configuration and local provider boundaries
|
||||||
|
|
||||||
`vite.config.js` delegates to `server/standalone/vite.config.js`, which loads
|
`vite.config.js` delegates to `server/standalone/vite.config.js`, which loads
|
||||||
|
|||||||
@@ -395,7 +395,11 @@ export async function resolveAnnotationTarget({
|
|||||||
// (which classifies building-vs-area and stitches compound multipolygons).
|
// (which classifies building-vs-area and stitches compound multipolygons).
|
||||||
// Point-like targets use the strict 'point' selection: exact-ish name + monument
|
// Point-like targets use the strict 'point' selection: exact-ish name + monument
|
||||||
// scale, else no polygon at all — the honest point beats a locality-word match.
|
// scale, else no polygon at all — the honest point beats a locality-word match.
|
||||||
fp = await fetchFootprint(lat, lon, matchName, scope, signal, pointLike ? 'point' : 'loose');
|
// Without a feature-name component, match the original ask and require
|
||||||
|
// identity or containment before replacing its geocoded anchor.
|
||||||
|
const footprintMode = pointLike ? 'point'
|
||||||
|
: (fromGeocode && !usablePrimary ? 'anchored' : 'loose');
|
||||||
|
fp = await fetchFootprint(lat, lon, matchName, scope, signal, footprintMode);
|
||||||
// A "grounds/compound/campus" phrase ("Texas Capitol grounds") names an ENCLOSING area. The
|
// A "grounds/compound/campus" phrase ("Texas Capitol grounds") names an ENCLOSING area. The
|
||||||
// primary footprint above returns the BUILDING (the dome) or null — neither is the grounds. The
|
// primary footprint above returns the BUILDING (the dome) or null — neither is the grounds. The
|
||||||
// real enclosing polygon (e.g. "Capitol Square", leisure=park) IS in OSM but only surfaces via a
|
// real enclosing polygon (e.g. "Capitol Square", leisure=park) IS in OSM but only surfaces via a
|
||||||
@@ -715,9 +719,9 @@ async function placesTextSearch(query, centerLat, centerLon, radiusM, signal) {
|
|||||||
/**
|
/**
|
||||||
* The canonical name of the geocoded feature: the address component whose own
|
* The canonical name of the geocoded feature: the address component whose own
|
||||||
* types match the result's feature type (e.g. the `neighborhood` component for a
|
* types match the result's feature type (e.g. the `neighborhood` component for a
|
||||||
* neighborhood result), falling back to the first component / leading label
|
* neighborhood result). Address-only responses may identify a landmark's city
|
||||||
* token. This is the user's INTENT, stripped of the trailing admin context that
|
* without naming the landmark itself. Return null in that case so the caller
|
||||||
* makes the raw utterance match the wrong-scope OSM feature.
|
* keeps the requested name instead of matching a nearby building in that city.
|
||||||
*/
|
*/
|
||||||
function extractPrimaryName(result) {
|
function extractPrimaryName(result) {
|
||||||
const resultTypes = new Set((result.types || []).map((t) => String(t).toLowerCase()));
|
const resultTypes = new Set((result.types || []).map((t) => String(t).toLowerCase()));
|
||||||
@@ -726,8 +730,7 @@ function extractPrimaryName(result) {
|
|||||||
const ct = (c.types || []).map((t) => String(t).toLowerCase());
|
const ct = (c.types || []).map((t) => String(t).toLowerCase());
|
||||||
if (ct.some((t) => t !== 'political' && resultTypes.has(t))) return c.long_name;
|
if (ct.some((t) => t !== 'political' && resultTypes.has(t))) return c.long_name;
|
||||||
}
|
}
|
||||||
if (comps[0]?.long_name) return comps[0].long_name;
|
return null;
|
||||||
return String(result.formatted_address || '').split(',')[0].trim() || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1222,6 +1225,7 @@ function perpDistance(p, a, b) {
|
|||||||
* 'loose' — default word-overlap scoring (generic POIs/compounds).
|
* 'loose' — default word-overlap scoring (generic POIs/compounds).
|
||||||
* 'strict' — named, non-building, district-sized areas only (neighborhood fallback).
|
* 'strict' — named, non-building, district-sized areas only (neighborhood fallback).
|
||||||
* 'point' — point-like targets: (almost) exactly-named, monument-scale polygons only.
|
* 'point' — point-like targets: (almost) exactly-named, monument-scale polygons only.
|
||||||
|
* 'anchored' — address-only geocodes: require name identity or anchor containment.
|
||||||
*
|
*
|
||||||
* @returns {Promise<null | { ring: Array<[number,number]>, kind: 'building'|'area', heightM: number|null }>}
|
* @returns {Promise<null | { ring: Array<[number,number]>, kind: 'building'|'area', heightM: number|null }>}
|
||||||
*/
|
*/
|
||||||
@@ -1487,7 +1491,7 @@ const POINTLIKE_AREA_CAP_M2 = 60_000;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Pick the best OSM polygon for a query from raw Overpass elements. `mode` selects the
|
* Pick the best OSM polygon for a query from raw Overpass elements. `mode` selects the
|
||||||
* acceptance contract ('loose' | 'strict' | 'point' — see fetchFootprint). Exported for
|
* acceptance contract ('loose' | 'strict' | 'point' | 'anchored' — see fetchFootprint). Exported for
|
||||||
* the unit tests, which pin the mode contracts with fixtures captured from live data.
|
* the unit tests, which pin the mode contracts with fixtures captured from live data.
|
||||||
*/
|
*/
|
||||||
export function selectFootprint(elements, targetLat, targetLon, query, mode = 'loose') {
|
export function selectFootprint(elements, targetLat, targetLon, query, mode = 'loose') {
|
||||||
@@ -1539,6 +1543,10 @@ export function selectFootprint(elements, targetLat, targetLon, query, mode = 'l
|
|||||||
}
|
}
|
||||||
|
|
||||||
const contains = pointInPolygon(targetLon, targetLat, coords);
|
const contains = pointInPolygon(targetLon, targetLat, coords);
|
||||||
|
if (mode === 'anchored' && !contains) {
|
||||||
|
const intentCoverage = queryWords.size ? nameOverlap / queryWords.size : 0;
|
||||||
|
if (!named || intentCoverage < 0.5 || completeness < 0.6) continue;
|
||||||
|
}
|
||||||
const centroid = ringCentroid(coords.map((p) => [p.lon, p.lat]));
|
const centroid = ringCentroid(coords.map((p) => [p.lon, p.lat]));
|
||||||
const distanceM = centroid
|
const distanceM = centroid
|
||||||
? approximateDistanceM(targetLat, targetLon, centroid.lat, centroid.lon)
|
? approximateDistanceM(targetLat, targetLon, centroid.lat, centroid.lon)
|
||||||
|
|||||||
@@ -362,3 +362,72 @@ test('ask-side admin bypass: admin level 2/3 result types never grant a township
|
|||||||
'both township-level admin result types stay guarded',
|
'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('https://maps.googleapis.com/')) {
|
||||||
|
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]));
|
||||||
|
});
|
||||||
Reference in new issue
Block a user