// Area annotations draw bundled US Census place and neighborhood outlines // (San Francisco DataSF first, then Who's On First) with no network lookup. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { FEATURE_SOURCE_METHODS } from '../sources/featureSource.js'; import { createAnnotationResolver } from './resolver.js'; import { polygonsContain } from '../data/adminBoundaries.js'; import { streetFootprint } from './outlineRungs.js'; function viewerAt(lat, lon, height = 20_000) { return { camera: { positionCartographic: { latitude: (lat * Math.PI) / 180, longitude: (lon * Math.PI) / 180, height, }, }, }; } /** A resolver whose boundary lookups and geocoder record every call. */ function harness(place = null, { outlineRungs } = {}) { const lookups = []; const featureSource = {}; for (const method of FEATURE_SOURCE_METHODS) featureSource[method] = async () => { lookups.push(method); return null; }; const geocodes = []; const placeSearch = { async geocode(query) { geocodes.push(query); return { place }; }, async textSearch() { geocodes.push('text-search'); return []; }, }; const { resolveAnnotationTarget } = createAnnotationResolver({ featureSource, ...(outlineRungs ? { outlineRungs } : {}), }); const resolve = (target, viewer, extra = {}) => resolveAnnotationTarget({ placeSearch, viewer, target, footprint: true, deferFootprint: true, ...extra, }); return { resolve, lookups, geocodes }; } const AUSTIN = viewerAt(30.2672, -97.7431, 3000); const SAN_FRANCISCO = viewerAt(37.76, -122.42, 3000); const LONDON = viewerAt(51.51, -0.16, 3000); function assertBundledArea(resolved, label) { assert.equal(resolved?.source, 'bundled', label); assert.equal(resolved.label, label); assert.equal(resolved.footprintKind, 'area'); assert.equal(resolved.synthesized, false); assert.equal( resolved.outlineCredit || null, null, 'bundled data is not an OSM outline', ); assert.equal(resolved.resolveOutline, undefined, 'drawn at once'); assert.deepEqual(resolved.ring[0], resolved.ring.at(-1), 'ring closed'); assert.ok(resolved.polygons.length >= 1); } test('a city name that is only a neighborhood alias goes to the geocoder', async () => { const { resolve, geocodes } = harness(); // Sé lists "São Paulo" among its names; the ask means the city. const saoPaulo = viewerAt(-23.55, -46.633, 60_000); const city = await resolve('São Paulo', saoPaulo); assert.notEqual(city?.label, 'Se'); assert.ok(geocodes.includes('São Paulo')); // A district ask still takes the alias. const district = await resolve('São Paulo', saoPaulo, { entityKind: 'district', }); assert.equal(district?.label, 'Se'); }); test('"Austin" over Austin draws the Census place with no lookup', async () => { const { resolve, lookups, geocodes } = harness(); for (const target of ['Austin', 'Austin, Texas', 'City of Austin']) { const resolved = await resolve(target, AUSTIN); assertBundledArea(resolved, 'Austin'); assert.ok(resolved.polygons.length >= 4, 'every part of the city'); } // A qualified name needs no view. const springfield = await resolve('Springfield, Illinois', AUSTIN); assertBundledArea(springfield, 'Springfield'); assert.ok(Math.abs(springfield.lat - 39.78) < 0.2); assert.deepEqual(geocodes, []); assert.deepEqual(lookups, []); }); test("the SF DataSF pack answers before Who's On First in San Francisco", async () => { const { resolve, lookups, geocodes } = harness(); const sf = JSON.parse( readFileSync( new URL( '../data/local_data/neighborhoods/san-francisco.json', import.meta.url, ), ), ); const mission = sf.features.find((f) => f.properties.name === 'Mission'); const missionRing = mission.geometry.coordinates[0]; for (const target of ['the Mission District', 'Mission', 'the Mission']) { const resolved = await resolve(target, SAN_FRANCISCO, { entityKind: 'district', }); assertBundledArea(resolved, 'Mission'); assert.deepEqual(resolved.polygons[0][0], missionRing, target); } // A name only WOF has in San Francisco falls through to it. const dogpatch = await resolve('Dogpatch', SAN_FRANCISCO); assertBundledArea(dogpatch, 'Dogpatch'); // A park asked for as a compound is not a neighborhood of its name. await resolve('Mission Dolores Park', SAN_FRANCISCO, { entityKind: 'compound', }); assert.equal(geocodes.filter((q) => q !== 'text-search').length, 1); assert.deepEqual(lookups, []); }); test("Who's On First neighborhoods resolve near the view", async () => { const { resolve, geocodes } = harness(); assertBundledArea(await resolve('Notting Hill', LONDON), 'Notting Hill'); assertBundledArea( await resolve('Le Marais', viewerAt(48.857, 2.352, 3000)), 'Le Marais', ); assertBundledArea( await resolve('Williamsburg', viewerAt(40.71, -73.96, 3000)), 'Williamsburg', ); assert.deepEqual(geocodes, []); }); test('campuses, qualified hoods and far names still go to the geocoder', async () => { const { resolve, geocodes } = harness(); // Stanford is a CDP, but a campus ask wants the campus. const stanford = await resolve('Stanford', viewerAt(37.43, -122.17, 3000), { entityKind: 'compound', }); assert.notEqual(stanford?.source, 'bundled'); await resolve('the Capitol grounds', AUSTIN); await resolve('Notting Hill', AUSTIN); assert.equal(geocodes.filter((q) => q !== 'text-search').length, 3); // A state settles a place name wherever the camera is: not Brooklyn's // Williamsburg, the Virginia city. const virginia = await resolve( 'Williamsburg, Virginia', viewerAt(40.71, -73.96, 3000), ); assertBundledArea(virginia, 'Williamsburg'); assert.ok(Math.abs(virginia.lat - 37.27) < 0.1, 'Virginia'); }); test('a geocoded city or neighborhood upgrades to the bundled outline', async () => { const city = harness({ lat: 30.2672, lng: -97.7431, label: 'Austin, TX, USA', name: 'Austin', types: ['locality', 'political'], }); const controller = new AbortController(); const credits = []; const cancelled = await city.resolve( 'Austin', { ...viewerAt(39, -100, 5_000_000), creditDisplay: { addStaticCredit: (credit) => credits.push(credit) }, }, { signal: controller.signal }, ); const pending = cancelled.resolveOutline(); controller.abort(); await assert.rejects(pending, { name: 'AbortError' }); assert.deepEqual( credits, [], 'a cancelled bundled continuation cannot mutate viewer credits', ); // Seen from far away: the view does not settle the name, the geocoder does. const resolved = await city.resolve('Austin', viewerAt(39, -100, 5_000_000)); assert.equal(resolved.source, 'geocode'); const outline = await resolved.resolveOutline(); assert.equal(outline.adminArea, 'Austin'); assert.ok(outline.polygons.length >= 4); assert.deepEqual(city.lookups, [], 'no boundary lookup'); const hood = harness({ lat: 51.5118, lng: -0.2046, label: 'Notting Hill, London, UK', name: 'Notting Hill', types: ['neighborhood', 'political'], }); const nottingHill = await hood.resolve( 'Notting Hill', viewerAt(48, 10, 5_000_000), ); const hoodOutline = await nottingHill.resolveOutline(); assert.equal(hoodOutline.adminArea, 'Notting Hill'); assert.deepEqual(hood.lookups, []); }); test('a district ask prefers a neighborhood; a place must hold the view', async () => { const { resolve, geocodes } = harness(); // "Richmond" over SF's Richmond District: never Richmond, California. const richmond = await resolve('Richmond', viewerAt(37.78, -122.48, 3000), { entityKind: 'district', }); assertBundledArea(richmond, 'Richmond District'); assert.ok(Math.abs(richmond.lat - 37.78) < 0.05, 'not the East Bay city'); // Omitted kind at a street-level view: the district in view, too. const bare = await resolve('Richmond', viewerAt(37.78, -122.48, 3000)); assertBundledArea(bare, 'Richmond District'); // Over the East Bay city, the city. const wide = await resolve('Richmond', viewerAt(37.94, -122.35, 40_000)); assert.equal(wide?.source, 'bundled'); assert.ok(wide.lat > 37.88, 'Richmond, California'); assert.deepEqual(geocodes, []); }); test('landmark-named neighborhoods wait for the landmark ladder', async () => { const manhattan = viewerAt(40.78, -73.965, 3000); const cases = [ ['Central Park', {}], ['Central Park', { entityKind: 'compound' }], ['Central Park', { intent: 'around_the_thing' }], ['Hyde Park', {}], ['Notting Hill', { intent: 'around_the_thing' }], ]; for (const [target, extra] of cases) { const { resolve, geocodes } = harness(); const viewer = target === 'Central Park' ? manhattan : LONDON; const resolved = await resolve(target, viewer, extra); assert.notEqual( resolved?.source, 'bundled', `${target} ${JSON.stringify(extra)}`, ); assert.ok(geocodes.length >= 1, `${target} is geocoded`); } // Named as a district, the WOF neighborhood answers. const { resolve } = harness(); const park = await resolve('Central Park', manhattan, { entityKind: 'district', }); assert.equal(park?.source, 'bundled'); // "Park Slope" ends in no landmark word: still a neighborhood. const slope = await resolve('Park Slope', viewerAt(40.671, -73.98, 3000)); assertBundledArea(slope, 'Park Slope'); }); /** Outline rungs whose Nominatim answers `answer` (null: nothing found). */ function rungsAnswering(answer) { const asked = []; return { asked, resolve: async (ctx, { base, finish }) => { asked.push({ scope: ctx.scope, target: ctx.target }); const baseResult = await base(); if (baseResult?.ring && !baseResult.synthesized) return baseResult; return answer ? finish(answer, ctx.scope) : baseResult; }, }; } const PARIS_PLACE = { lat: 48.8566, lng: 2.3522, label: 'Paris, France', name: 'Paris', types: ['locality', 'political'], }; const square = (lat, lon, d) => [ [lon - d, lat - d], [lon + d, lat - d], [lon + d, lat + d], [lon - d, lat + d], [lon - d, lat - d], ]; test('a courtyard outline with no filled candidate anchor remains a point', async () => { const lat = 0.0005; const lon = 0.0005; const ring = square(lat, lon, 0.0005); const hole = square(lat, lon, 0.0002); const polygons = [[ring, hole]]; const { resolve } = harness(null, { outlineRungs: rungsAnswering({ ring, polygons, kind: 'building', heightM: 20, }), }); const options = { latitude: lat, longitude: lon, entityKind: 'building' }; const pending = await resolve( 'this building', viewerAt(lat, lon, 1000), options, ); const outline = await pending.resolveOutline(); assert.equal( outline, null, 'neither the mean nor the courtyard point is filled', ); const inline = await resolve('this building', viewerAt(lat, lon, 1000), { ...options, deferFootprint: false, }); assert.equal(inline.ring, null); assert.deepEqual( [inline.lat, inline.lon], [lat, lon], 'keep the honest point', ); for (const resolved of [outline, inline]) { if (resolved?.polygons) assert.ok(polygonsContain(resolved.polygons, resolved.lat, resolved.lon)); } }); test('a bent street keeps its outline when the anchor sits beside the centreline', async () => { // 500 m east then 500 m north: the ring mean lands off the 22 m ribbon. const lat0 = 30.27; const lon0 = -97.74; const dLon = 500 / (111320 * Math.cos((lat0 * Math.PI) / 180)); const dLat = 500 / 111320; const line = [ [lon0, lat0], [lon0 + dLon / 2, lat0], [lon0 + dLon, lat0], [lon0 + dLon, lat0 + dLat / 2], [lon0 + dLon, lat0 + dLat], ]; const footprint = streetFootprint({ lines: [line] }); // A geocoded point 15 m north of the first leg, outside the ribbon. const lat = lat0 + 15 / 111320; const lon = lon0 + dLon / 2; assert.equal(polygonsContain(footprint.polygons, lat, lon), false); const { resolve } = harness( { lat, lng: lon, name: 'Bent Street', label: 'Bent St', types: ['route'] }, { outlineRungs: rungsAnswering(footprint) }, ); const pending = await resolve('Bent Street', viewerAt(lat, lon, 3000), { entityKind: 'street', }); const outline = await pending.resolveOutline(); assert.equal(outline?.ring, footprint.ring, 'the street is still drawn'); assert.ok(polygonsContain(outline.polygons, outline.lat, outline.lon)); assert.ok(Math.abs(outline.lat - lat0) < 1e-7, 'centred on the street'); assert.ok(Math.abs(outline.lon - lon) < 1e-7); // A same-name street that never comes near the anchor is not that street. const far = lat0 + 300 / 111320; const away = harness( { lat: far, lng: lon, name: 'Bent Street', label: 'Bent St', types: ['route'], }, { outlineRungs: rungsAnswering(footprint) }, ); const distant = await away.resolve('Bent Street', viewerAt(far, lon, 3000), { entityKind: 'street', }); assert.equal((await distant.resolveOutline())?.ring ?? null, null); }); test('local dateline outlines share a frame for area, centering and filled anchors', async () => { const ring = [ [179.9995, -0.0005], [-179.9995, -0.0005], [-179.9995, 0.0005], [179.9995, 0.0005], [179.9995, -0.0005], ]; const hole = [ [179.9998, -0.0003], [-179.9998, -0.0003], [-179.9998, 0.0003], [179.9998, 0.0003], [179.9998, -0.0003], ]; const polygons = [[ring, hole]]; const { resolve } = harness(null, { outlineRungs: rungsAnswering({ ring, polygons, kind: 'building', heightM: 20, }), }); for (const deferFootprint of [true, false]) { for (const [lat, lon, filled] of [ [0.0004, 179.9996, true], [0, 179.99999, false], [0.0004, 0, false], ]) { const result = await resolve('this building', viewerAt(lat, lon, 1000), { latitude: lat, longitude: lon, entityKind: 'building', deferFootprint, }); const outline = deferFootprint ? await result.resolveOutline() : result; if (!filled) { assert.equal(outline?.ring ?? null, null); continue; } assert.equal(outline.ring, ring); assert.equal(outline.polygons, polygons); assert.deepEqual( [outline.lat, outline.lon], [lat, lon], 'keep the filled shell anchor', ); } } const plain = harness(null, { outlineRungs: rungsAnswering({ ring, kind: 'building', heightM: 20 }), }); const pending = await plain.resolve( 'this building', viewerAt(0, 179.9999, 1000), { latitude: 0, longitude: 179.9999, entityKind: 'building', }, ); const centered = await pending.resolveOutline(); assert.equal(centered.ring, ring); assert.ok( Math.abs(centered.lon) > 179.999 && centered.lon >= -180 && centered.lon < 180, ); }); test('a coarse bundled admin-1 city waits for a finer outline', async () => { const overParis = viewerAt(48.8566, 2.3522, 40_000); // The outer-ring vertex mean is in the hole; the resolved anchor is in // the shell. Completion must not move that valid point into the hole. const ring = square(48.8566, 2.3822, 0.04); const hole = square(48.8566, 2.3822, 0.02); const polygons = [[ring, hole]]; const rungs = rungsAnswering({ ring, polygons, kind: 'area', heightM: null }); const fine = harness(PARIS_PLACE, { outlineRungs: rungs }); const resolved = await fine.resolve('Paris', overParis); assert.equal( resolved.source, 'geocode', 'the 11-vertex admin-1 is not drawn at once', ); const outline = await resolved.resolveOutline(); assert.deepEqual(outline.ring, ring, 'the finer outline wins'); assert.deepEqual(outline.polygons, polygons); assert.equal(outline.lat, PARIS_PLACE.lat); assert.equal(outline.lon, PARIS_PLACE.lng); assert.ok(polygonsContain(outline.polygons, outline.lat, outline.lon)); assert.deepEqual(rungs.asked, [{ scope: 'city', target: 'Paris' }]); const inline = await fine.resolve('Paris', overParis, { deferFootprint: false, }); assert.deepEqual(inline.polygons, polygons); assert.equal(inline.lat, outline.lat); assert.equal(inline.lon, outline.lon); // Nothing finer (Nominatim off, busy or capped): the bundled shape, not a point. const coarse = harness(PARIS_PLACE, { outlineRungs: rungsAnswering(null) }); const fallback = await ( await coarse.resolve('Paris', overParis) ).resolveOutline(); assert.equal(fallback.adminArea, 'Paris'); assert.ok(fallback.ring.length < 32); // No geocoder answer at all: the bundled shape at once. const offline = harness(null, { outlineRungs: rungsAnswering(null) }); const direct = await offline.resolve('Paris', overParis); assert.equal(direct.source, 'bundled'); assert.equal(direct.label, 'Paris'); const overDallas = viewerAt(32.7767, -96.797, 20_000); assert.equal( await offline.resolve('Paris', overDallas), null, 'a missing geocode cannot select the distant coarse homonym', ); assert.equal( await coarse.resolve('Paris', overDallas), null, 'a rejected foreign geocode cannot restore the same distant polygon', ); const outsideParis = harness( { ...PARIS_PLACE, lat: 48.9, lng: 2.6 }, { outlineRungs: rungsAnswering(null) }, ); const anchored = await outsideParis.resolve('Paris', overParis); assert.equal(anchored.source, 'geocode'); assert.equal( await anchored.resolveOutline(), undefined, 'a nearby anchor outside Paris preserves the transient base, not its coarse polygon', ); }); test('deferred outlines preserve provider administrative identity metadata', async () => { const overParis = viewerAt(48.8566, 2.3522, 40_000); const ring = square(48.8566, 2.3522, 0.05); const { resolve } = harness(PARIS_PLACE, { outlineRungs: rungsAnswering({ ring, kind: 'area', heightM: null, adminArea: 'Île-de-France', adminLevel: 'admin1', }), }); const outline = await (await resolve('Paris', overParis)).resolveOutline(); assert.equal(outline.adminArea, 'Île-de-France'); assert.equal(outline.adminLevel, 'admin1'); }); test('an explicitly distant area keeps its deferred outline', async () => { const kathmandu = { lat: 27.7172, lng: 85.324, label: 'Kathmandu, Nepal', name: 'Kathmandu', types: ['locality', 'political'], }; const ring = square(27.7172, 85.324, 0.05); const { resolve } = harness(kathmandu, { outlineRungs: rungsAnswering({ ring, kind: 'area', heightM: null }), }); const resolved = await resolve('Kathmandu', SAN_FRANCISCO, { allowDistant: true, }); assert.equal(resolved.source, 'geocode'); assert.deepEqual( (await resolved.resolveOutline()).ring, ring, 'the deferred centroid gate honors the same explicit distance policy as the anchor', ); }); test('detailed admin-1 units and admin asks keep the bundled shape', async () => { const { resolve } = harness(null, { outlineRungs: rungsAnswering(null) }); for (const [name, lat, lon] of [ ['Berlin', 52.52, 13.4], ['Bavaria', 48.9, 11.4], ['Texas', 31, -99], ]) { const resolved = await resolve(name, viewerAt(lat, lon, 400_000)); assert.equal(resolved?.source, 'bundled', name); } // Countries are never coarse cities, however few vertices. const singapore = await resolve('Singapore', viewerAt(1.35, 103.82, 60_000)); assert.equal(singapore?.source, 'bundled'); // An explicit admin ask takes the admin unit as it is. const dc = await resolve('Paris', viewerAt(48.8566, 2.3522, 40_000), { entityKind: 'state', }); assert.equal(dc?.source, 'bundled'); }); test('a campus ask reaches the campus, not the Census place of its town', async () => { const stanfordTown = { lat: 37.4241, lng: -122.1661, label: 'Stanford, CA, USA', name: 'Stanford', types: ['locality', 'political'], }; const campus = square(37.4275, -122.1697, 0.01); for (const ask of ['Stanford University', 'Stanford campus']) { const rungs = rungsAnswering({ ring: campus, kind: 'area', heightM: null }); const { resolve } = harness(stanfordTown, { outlineRungs: rungs }); const resolved = await resolve(ask, viewerAt(37.43, -122.17, 8000)); assert.notEqual(resolved?.source, 'bundled', ask); const outline = await resolved.resolveOutline(); assert.deepEqual(outline.ring, campus, ask); assert.equal( outline.outlineCredit, null, 'an injected outline is not inferred OSM', ); assert.equal( rungs.asked[0].scope, 'compound', `${ask}: past the Census rung`, ); } // "Stanford" said to be a compound is the campus too. const kindRungs = rungsAnswering({ ring: campus, kind: 'area', heightM: null, }); const byKind = harness(stanfordTown, { outlineRungs: kindRungs }); const compound = await ( await byKind.resolve('Stanford', viewerAt(37.43, -122.17, 8000), { entityKind: 'compound', }) ).resolveOutline(); assert.deepEqual(compound.ring, campus); assert.equal(kindRungs.asked[0].scope, 'compound'); const labelRungs = rungsAnswering({ ring: campus, kind: 'area', heightM: null, }); const byLabel = harness(stanfordTown, { outlineRungs: labelRungs }); const labelled = await byLabel.resolve( 'Stanford', viewerAt(37.43, -122.17, 8000), { labelHint: 'Stanford campus' }, ); assert.notEqual(labelled.source, 'bundled'); assert.deepEqual((await labelled.resolveOutline()).ring, campus); assert.equal(labelRungs.asked[0].scope, 'compound'); // "Stanford" alone is the town (the Stanford CDP), and a town named after a // college stays a town. const { resolve } = harness(stanfordTown, { outlineRungs: rungsAnswering(null), }); const town = await ( await resolve('Stanford', viewerAt(39, -100, 5_000_000)) ).resolveOutline(); assert.equal(town.adminArea, 'Stanford'); const station = harness( { lat: 30.628, lng: -96.3344, label: 'College Station, TX, USA', name: 'College Station', types: ['locality', 'political'], }, { outlineRungs: rungsAnswering(null) }, ); const collegeStation = await ( await station.resolve('College Station', viewerAt(39, -100, 5_000_000)) ).resolveOutline(); assert.equal(collegeStation.adminArea, 'College Station'); }); test('a grounds ask never takes the state the geocoder typed', async () => { const texas = { lat: 31.0, lng: -99.0, label: 'Texas, USA', name: 'Texas', types: ['administrative_area_level_1', 'political'], }; for (const [ask, extra] of [ ['the Texas Capitol grounds', {}], ['Texas Capitol', { entityKind: 'compound' }], // The grounds wording is only in the label. ['Texas Capitol', { labelHint: 'Capitol grounds' }], ]) { const rungs = rungsAnswering(null); const { resolve } = harness(texas, { outlineRungs: rungs }); const resolved = await resolve(ask, viewerAt(31, -99, 3_000_000), extra); assert.notEqual(resolved?.source, 'bundled', ask); const outline = await resolved.resolveOutline(); assert.notEqual(outline?.adminArea, 'Texas', ask); assert.equal(rungs.asked[0].scope, 'compound', ask); } }); test('configured OSM geometry retains its origin through inline, deferred and cached base results', async () => { const ring = square(31, -99, 0.001); let calls = 0; const resolver = createAnnotationResolver({ boundarySource: { query: async () => { calls++; return [ { type: 'way', id: 1, tags: { name: 'Library', building: 'yes' }, geometry: ring.map(([lon, lat]) => ({ lon, lat })), }, ]; }, }, outlineRungs: { resolve: (_ctx, { base }) => base() }, }); for (const deferFootprint of [false, true]) { const result = await resolver.resolveAnnotationTarget({ viewer: viewerAt(31, -99, 3000), latitude: 31, longitude: -99, target: 'Library', entityKind: 'building', footprint: true, deferFootprint, }); const outline = deferFootprint ? await result.resolveOutline() : result; assert.equal(outline.footprintKind, 'building'); assert.equal(outline.outlineCredit, 'osm'); assert.deepEqual(outline.ring, ring); } assert.equal(calls, 1, 'cached geometry retains its original provider'); });