diff --git a/apps/web/src/lib/search/server/geocode.test.ts b/apps/web/src/lib/search/server/geocode.test.ts index e9d15ea..d07a2a2 100644 --- a/apps/web/src/lib/search/server/geocode.test.ts +++ b/apps/web/src/lib/search/server/geocode.test.ts @@ -13,7 +13,7 @@ const louisville = [ ]; describe('geocodeLocation', () => { - it('queries Nominatim and returns the top result as numeric coords', async () => { + it('geocodes through the shared client and returns the top result as numeric coords', async () => { const fetchImpl = fakeFetch(200, louisville); const result = await geocodeLocation('Louisville, KY', fetchImpl); @@ -21,13 +21,12 @@ describe('geocodeLocation', () => { expect(fetchImpl).toHaveBeenCalledTimes(1); const [input, init] = fetchImpl.mock.calls[0] as unknown as [URL | string, RequestInit]; const url = new URL(String(input)); - // URL is sourced from the shared geocoder constant, not a local literal. + // URL is sourced from the shared geocoder client, not a local literal. const shared = new URL(DEFAULT_GEOCODER_URL); expect(url.hostname).toBe(shared.hostname); expect(url.pathname).toBe(shared.pathname); expect(url.hostname).toBe('nominatim.openstreetmap.org'); expect(url.searchParams.get('q')).toBe('Louisville, KY'); - expect(url.searchParams.get('format')).toBe('jsonv2'); expect(url.searchParams.get('limit')).toBe('1'); // Nominatim's usage policy requires an identifying User-Agent; default // comes from the shared env-driven helper. @@ -52,6 +51,20 @@ describe('geocodeLocation', () => { expect(new Headers(init.headers).get('user-agent')).toBe('custom-agent/9.9 (https://example.test)'); }); + it('honors a configured GEOCODER_URL/KEY (the shared client appends the key server-side)', async () => { + const fetchImpl = fakeFetch(200, louisville); + + await geocodeLocation('Louisville, KY', fetchImpl, { + GEOCODER_URL: 'https://us1.locationiq.com/v1/search', + GEOCODER_KEY: 'tok' + }); + + const [input] = fetchImpl.mock.calls[0] as unknown as [URL | string, RequestInit]; + const url = new URL(String(input)); + expect(url.hostname).toBe('us1.locationiq.com'); + expect(url.searchParams.get('key')).toBe('tok'); + }); + it('returns null when nothing matches', async () => { expect(await geocodeLocation('zzzz no such place', fakeFetch(200, []))).toBeNull(); }); diff --git a/apps/web/src/lib/search/server/geocode.ts b/apps/web/src/lib/search/server/geocode.ts index f2599f2..e3b5e5b 100644 --- a/apps/web/src/lib/search/server/geocode.ts +++ b/apps/web/src/lib/search/server/geocode.ts @@ -7,11 +7,14 @@ // page. Heavier-traffic compliance (app-wide rate limiting, caching) is still // a follow-up before high-volume exposure. // -// Base URL and User-Agent come from the shared geocoder module (one canonical -// source, no duplicate literals). This path stays on public Nominatim — it does -// NOT honor a configured GEOCODER_URL, because that endpoint may be a keyed -// LocationIQ URL and this hot-path forward search sends no ?key= (it would 401). -import { DEFAULT_GEOCODER_URL, resolveGeocoderUserAgent, type GeocoderEnv } from './geocoder'; +// This is a thin adapter over the shared createGeocoder client — the single +// forward-geocode client for every server-side path (near-me, the address-entry +// form's /api/geocoding endpoint, and the bulk drip). It therefore honors a +// configured GEOCODER_URL/GEOCODER_KEY (keyed LocationIQ) like the drip does: +// the key is appended server-side by the shared client and never reaches the +// browser, so the earlier "don't honor GEOCODER_URL, we send no ?key=" rationale +// no longer applies. Default remains public Nominatim when no key/URL is set. +import { createGeocoder, type GeocoderEnv } from './geocoder'; export type GeocodeResult = { lat: number; @@ -25,29 +28,7 @@ export async function geocodeLocation( fetchImpl: typeof fetch = fetch, env: GeocoderEnv = {} ): Promise { - const url = new URL(DEFAULT_GEOCODER_URL); - url.searchParams.set('q', q); - url.searchParams.set('format', 'jsonv2'); - url.searchParams.set('limit', '1'); - - const response = await fetchImpl(url, { - headers: { accept: 'application/json', 'user-agent': resolveGeocoderUserAgent(env) } - }); - if (!response.ok) { - throw new Error(`geocode request failed: ${response.status}`); - } - - const results = (await response.json()) as { - lat?: string; - lon?: string; - display_name?: string; - }[]; - const top = results[0]; - if (!top) return null; - - const lat = Number(top.lat); - const lng = Number(top.lon); - if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null; - - return { lat, lng, label: top.display_name ?? q }; + const point = await createGeocoder(env, fetchImpl).geocode(q); + if (!point) return null; + return { lat: point.lat, lng: point.lng, label: point.label ?? q }; }