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
This commit is contained in:
Niccolò Saggese committed 2026-09-21 11:30:49 +02:00
1 parent 0dbde1e36c
commit c5b1d41cba
15 files changed
+436 -47

No files matched your search

+14 -9
View File
@@ -40,18 +40,23 @@ export function createDefaultPlaceSearch({
...(selected
? [selected]
: [
// Google answers through the local proxy, which holds the server
// key: its Geocoding web service refuses referrer-restricted keys,
// so calling it from here would force the bundled browser key to be
// left unrestricted (#363). That key still gates the provider,
// because a keyless session must not spend a request to learn it
// has no Google: the proxy's "not configured" reads as an
// unanswered provider, which stops the chain caching the negative
// Photon or Nominatim then produces.
createGoogleGeocoder({
request(query, { bias, signal }) {
const key = resolveApiKey?.();
if (!key) return null;
const url = new URL(
endpoints.geocode ||
'https://maps.googleapis.com/maps/api/geocode/json',
if (!resolveApiKey?.()) return null;
const params = new URLSearchParams({ address: query });
if (bias) params.set('bounds', bias);
return fetchImpl(
`${endpoints.geocode || '/api/google/geocode'}?${params}`,
{ signal },
);
url.searchParams.set('address', query);
url.searchParams.set('key', key);
if (bias) url.searchParams.set('bounds', bias);
return fetchImpl(url.toString(), { signal });
},
}),
createPhotonGeocoder({ fetchImpl, endpoint: endpoints.photon }),
+7 -8
View File
@@ -53,7 +53,9 @@ export function createHttpGeospatialProvider({
endpoints = {},
} = {}) {
const urls = {
reverse: 'https://maps.googleapis.com/maps/api/geocode/json',
// Same-origin like its siblings below: Google's Geocoding web service
// refuses referrer-restricted keys, so the key stays on the server (#363).
reverse: '/api/google/reverse-geocode',
textSearch: '/api/google/text-search',
nearby: '/api/google/nearby-places',
route: '/api/route',
@@ -88,14 +90,11 @@ export function createHttpGeospatialProvider({
},
routeProfiles: ['foot', 'car', 'bike'],
async reverseGeocode(latitude, longitude, options) {
const key = resolveApiKey?.();
if (resolveApiKey && !key) return null;
// The browser key gates the provider without travelling with the
// request: the proxy supplies the key Google actually sees.
if (resolveApiKey && !resolveApiKey()) return null;
return normalizeGoogleReverse(
await json(
urls.reverse,
{ latlng: `${latitude},${longitude}`, ...(key ? { key } : {}) },
options,
),
await json(urls.reverse, { lat: latitude, lon: longitude }, options),
);
},
async textSearch(query, { latitude, longitude, radiusM = 6000 }, options) {
+1 -1
View File
@@ -20,7 +20,7 @@ test('standalone geocoding falls back after Google connection, JSON and refusal
for (const fail of [() => { throw new Error('offline'); }, () => new Response('invalid json'), () => Response.json({ status: 'REQUEST_DENIED' })]) {
const urls = [];
const service = createStandalonePlaceSearch({ resolveApiKey: () => 'fixture', fetchImpl: async (url) => {
urls.push(url); return url.includes('maps.googleapis.com') ? fail() : hit();
urls.push(url); return url.includes('/api/google/geocode') ? fail() : hit();
} });
const result = await service.geocode('Hanoi');
assert.equal(result.place.name, 'Hà Nội');