feat(search): compose Google and Photon through an explicit provider service

This commit is contained in:
Sameh Khamis committed 2026-09-12 15:38:43 -07:00
commit 57d7f9fc23
30 files changed
+1621 -151

No files matched your search

+18 -28
View File
@@ -1,5 +1,6 @@
import * as Cesium from 'cesium';
import { viewportBias, placesNearViewRecovery } from './annotations/annotationResolver.js';
import { unavailablePlaceSearch } from './search/placeSearch.js';
/**
* Points of Interest per city.
@@ -342,46 +343,35 @@ export function findPoiByName(query) {
export const CANCELLED_SEARCH = Object.freeze({ cancelled: true });
/**
* Geocode a place name using Google Geocoding API, then fly there at a scale
* Geocode a place name through the supplied service, then fly there at a scale
* appropriate to the request. Countries and cities use their viewport by
* default; precise landmarks/buildings use close landmark framing.
*/
export async function searchAndFlyTo(viewer, query, options = {}) {
const apiKey = window.__GOOGLE_MAPS_API_KEY__ || import.meta.env.GOOGLE_MAPS_API_KEY;
if (!apiKey) throw new Error('No Google Maps API key available for geocoding');
const { placeSearch = unavailablePlaceSearch, signal } = options;
signal?.throwIfAborted();
const beforeFly = typeof options.beforeFly === 'function' ? options.beforeFly : null;
const mayFly = () => beforeFly === null || beforeFly() !== false;
// Viewport-biased geocode — the same bias annotationResolver's geocodePlace uses:
// "Sixth Street" spoken over Austin must prefer the Sixth Street on screen, not a
// same-named road in another city (or the wrong end of town — the W 6th vs E 6th bug).
let url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(query)}&key=${apiKey}`;
const bias = viewportBias(viewer);
if (bias) url += `&bounds=${bias}`;
const response = await fetch(url);
const data = await response.json();
const result = (data.status === 'OK' && data.results?.length) ? data.results[0] : null;
let lat = result?.geometry.location.lat;
let lng = result?.geometry.location.lng;
let label = result ? result.formatted_address : null;
const mayFly = () => !signal?.aborted && (beforeFly === null || beforeFly() !== false);
const outcome = await placeSearch.geocode(query, { bias: viewportBias(viewer), signal });
signal?.throwIfAborted();
const result = outcome.place;
let lat = result?.lat;
let lng = result?.lng;
let label = result?.label || query;
let types = result?.types || [];
let viewport = result ? (result.geometry.bounds || result.geometry.viewport) : null;
let viewport = result?.viewport || null;
// Places-near-view recovery (annotationResolver's twin): a missed geocode, or one
// that landed implausibly far from the view centre, snaps back to a view-biased
// Places hit within the trust bound — "the Capitol" means the one on screen.
const recovered = await placesNearViewRecovery(viewer, query, result ? { lat, lon: lng } : null);
// Nearby landmark recovery retains precedence over a fallback geocoder hit.
const recovered = await placesNearViewRecovery(viewer, query,
result && !outcome.fallbackUsed ? { lat, lon: lng } : null, signal);
signal?.throwIfAborted();
if (recovered) {
lat = recovered.lat;
lng = recovered.lon;
label = recovered.label || label || query;
label = recovered.label || label;
types = recovered.types || [];
viewport = placesViewportToBounds(recovered.viewport) || viewport;
} else if (!result) {
return null;
}
} else if (!result) return null;
const requestedRange = finitePositive(options.range);
const duration = finitePositive(options.duration) || 3.0;