@happyvertical/geo 0.74.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENT.md ADDED
@@ -0,0 +1,33 @@
1
+ # @happyvertical/geo
2
+
3
+ <!-- BEGIN AGENT:GENERATED -->
4
+ ## Purpose
5
+ Standardized geographical information interface supporting Google Maps and OpenStreetMap
6
+
7
+ ## Package Map
8
+ - Package: `@happyvertical/geo`
9
+ - Hierarchy path: `@happyvertical/sdk > packages > geo`
10
+ - Workspace position: `12 of 30` local packages
11
+ - Internal dependencies: `@happyvertical/cache`, `@happyvertical/utils`
12
+ - Internal dependents: none
13
+ - Knowledge graph files: `AGENT.md`, `metadata.json`, `ecosystem-manifest.json`
14
+
15
+ ## Build & Test
16
+ ```bash
17
+ pnpm --filter @happyvertical/geo build
18
+ pnpm --filter @happyvertical/geo test
19
+ pnpm --filter @happyvertical/geo clean
20
+ ```
21
+
22
+ ## Agent Correction Loops
23
+ - If module resolution or export errors mention a workspace dependency, build the dependency first (`pnpm --filter @happyvertical/cache build`, `pnpm --filter @happyvertical/utils build`) and then rerun `pnpm --filter @happyvertical/geo build`.
24
+ - If tests or exports fail after API, type, or bundle changes, run `pnpm --filter @happyvertical/geo clean` followed by `pnpm --filter @happyvertical/geo build` and `pnpm --filter @happyvertical/geo test`.
25
+ - If failures span multiple packages or Turborepo ordering looks wrong, run `pnpm build` and `pnpm typecheck` from the repo root before retrying package-scoped commands.
26
+
27
+ ## Ecosystem Relationships
28
+ - Provides: Standardized geographical information interface supporting Google Maps and OpenStreetMap
29
+ - Implements: none
30
+ - Requires: @happyvertical/cache, @happyvertical/utils, @googlemaps/google-maps-services-js
31
+ - Stability: stable (Primary package surface is described as implemented and production-oriented.)
32
+ <!-- END AGENT:GENERATED -->
33
+
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright <2025> <Happy Vertical Corporation>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,194 @@
1
+ ---
2
+ id: geo
3
+ title: "@happyvertical/geo: Geographical Information"
4
+ sidebar_label: "@happyvertical/geo"
5
+ sidebar_position: 5
6
+ ---
7
+
8
+ # @happyvertical/geo
9
+
10
+ Geocoding, reverse geocoding, and static map generation with a unified adapter interface. Supports Google Maps and OpenStreetMap (Nominatim) for geocoding, and Mapbox and Google Maps for static map images. Results are cached in memory automatically.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pnpm add @happyvertical/geo
16
+ ```
17
+
18
+ Peer dependencies: `@happyvertical/cache`, `@happyvertical/utils`.
19
+
20
+ ## Usage
21
+
22
+ ### Geocoding
23
+
24
+ ```typescript
25
+ import { getGeoAdapter } from '@happyvertical/geo';
26
+
27
+ const adapter = await getGeoAdapter({
28
+ provider: 'google',
29
+ apiKey: process.env.GOOGLE_MAPS_API_KEY!,
30
+ });
31
+
32
+ const results = await adapter.lookup('Eiffel Tower, Paris');
33
+ console.log(results[0].name); // Formatted address
34
+ console.log(results[0].latitude); // 48.8583701
35
+ console.log(results[0].countryCode); // FR
36
+
37
+ const locations = await adapter.reverseGeocode(48.8584, 2.2945);
38
+ ```
39
+
40
+ ### OpenStreetMap (no API key required)
41
+
42
+ ```typescript
43
+ const adapter = await getGeoAdapter({
44
+ provider: 'openstreetmap',
45
+ rateLimitDelay: 1000, // ms between requests (default)
46
+ });
47
+
48
+ const results = await adapter.lookup('Big Ben, London');
49
+ ```
50
+
51
+ ### POI (Point-of-Interest) search
52
+
53
+ Both providers implement `findPoisNear(lat, lon, radiusMeters, options?)`
54
+ for discovering businesses, landmarks, and amenities around a coordinate.
55
+ It's an optional method on the adapter — feature-detect before calling:
56
+
57
+ ```typescript
58
+ if (typeof adapter.findPoisNear === 'function') {
59
+ const cafes = await adapter.findPoisNear(48.8566, 2.3522, 300, {
60
+ types: ['cafe'],
61
+ limit: 10,
62
+ });
63
+ for (const cafe of cafes) {
64
+ console.log(cafe.name, '@', cafe.latitude, cafe.longitude);
65
+ }
66
+ }
67
+ ```
68
+
69
+ **Google** routes the request through the Places API (Nearby Search) so the
70
+ API key needs the Places API enabled in Google Cloud in addition to
71
+ Geocoding. The first entry of `options.types` becomes the request's `type`
72
+ filter; additional entries fan out across separate requests and the results
73
+ are deduped by `place_id`. Max radius 50 000 m per Places API.
74
+
75
+ **OpenStreetMap** uses the public [Overpass API][overpass]. No key, but
76
+ the same community use-policy as Nominatim applies — cache aggressively
77
+ and reuse a `rateLimitDelay` that matches your traffic. When `types` is
78
+ omitted, the query looks across `amenity`, `shop`, `tourism`, `leisure`,
79
+ `office`, `historic`, and `craft` tag keys. When supplied, values are
80
+ matched against each of those keys so you can pass `['cafe']` or
81
+ `['supermarket']` without knowing the exact tag.
82
+
83
+ [overpass]: https://wiki.openstreetmap.org/wiki/Overpass_API
84
+
85
+ ### Environment Variable Configuration
86
+
87
+ Set `HAVE_GEO_PROVIDER`, `HAVE_GEO_TIMEOUT`, `HAVE_GEO_MAX_RESULTS`, `HAVE_GEO_RATE_LIMIT_DELAY`, `HAVE_GEO_USER_AGENT`, and `GOOGLE_MAPS_API_KEY` to configure without passing options:
88
+
89
+ ```typescript
90
+ const adapter = await getGeoAdapter(); // reads from env
91
+ ```
92
+
93
+ User-provided options always take precedence over environment variables.
94
+
95
+ ### Static Maps
96
+
97
+ Generate static map URLs or fetch map images for embedding:
98
+
99
+ ```typescript
100
+ import { getStaticMapUrl, fetchStaticMap, getOGMapUrl } from '@happyvertical/geo';
101
+
102
+ // Mapbox URL (default provider)
103
+ const url = getStaticMapUrl(53.5461, -113.4938, {
104
+ provider: 'mapbox',
105
+ zoom: 14,
106
+ width: 1200,
107
+ height: 630,
108
+ });
109
+
110
+ // Google Maps URL
111
+ const googleUrl = getStaticMapUrl(53.5461, -113.4938, {
112
+ provider: 'google',
113
+ googleMapType: 'roadmap',
114
+ });
115
+
116
+ // Fetch image as Buffer
117
+ const result = await fetchStaticMap(53.5461, -113.4938, { provider: 'mapbox' });
118
+ await fs.writeFile('map.png', result.buffer);
119
+
120
+ // OG-sized map (1200x630) convenience function
121
+ const ogUrl = getOGMapUrl(53.5461, -113.4938);
122
+ ```
123
+
124
+ ## API
125
+
126
+ ### `getGeoAdapter(options?): Promise<GeoAdapter>`
127
+
128
+ Factory function returning a geocoding adapter. Options are a discriminated union on `provider`:
129
+
130
+ | Option | Google | OSM | Description |
131
+ |--------|--------|-----|-------------|
132
+ | `provider` | `'google'` | `'openstreetmap'` | Required (or set `HAVE_GEO_PROVIDER`) |
133
+ | `apiKey` | required | — | Google Maps API key |
134
+ | `timeout` | optional | optional | Request timeout ms (default: 10000) |
135
+ | `maxResults` | optional | optional | Max results (default: 10) |
136
+ | `userAgent` | — | optional | Custom User-Agent for Nominatim |
137
+ | `rateLimitDelay` | — | optional | Delay between requests ms (default: 1000) |
138
+
139
+ ### `GeoAdapter` Interface
140
+
141
+ ```typescript
142
+ interface GeoAdapter {
143
+ lookup(query: string): Promise<Location[]>;
144
+ reverseGeocode(latitude: number, longitude: number): Promise<Location[]>;
145
+ }
146
+ ```
147
+
148
+ ### `Location`
149
+
150
+ ```typescript
151
+ interface Location {
152
+ id: string;
153
+ type: 'country' | 'region' | 'city' | 'address' | 'point_of_interest' | 'unknown';
154
+ name: string;
155
+ latitude: number;
156
+ longitude: number;
157
+ addressComponents: {
158
+ streetNumber?: string; streetName?: string; city?: string;
159
+ region?: string; country?: string; postalCode?: string;
160
+ };
161
+ countryCode: string;
162
+ timezone?: string;
163
+ raw: any;
164
+ }
165
+ ```
166
+
167
+ ### Static Map Functions
168
+
169
+ - `getStaticMapUrl(lat, lng, options?): string` — Generate a Mapbox or Google static map URL.
170
+ - `fetchStaticMap(lat, lng, options?): Promise<StaticMapResult>` — Fetch the map image as a Buffer.
171
+ - `getOGMapUrl(lat, lng, options?): string` — Convenience for 1200x630 Open Graph maps.
172
+
173
+ `StaticMapOptions` supports `provider` (`'mapbox'` | `'google'`), `width`, `height`, `zoom`, `markerColor`, `showMarker`, `mapboxToken`/`googleApiKey`, `mapboxStyle`, `googleMapType`, `scale`, and `markers`.
174
+
175
+ ### Error Classes
176
+
177
+ All extend `GeoError`:
178
+
179
+ - `GeoError` — Base error with `code` and `provider` fields
180
+ - `InvalidQueryError` — Empty or malformed query
181
+ - `RateLimitError` — Provider rate limit exceeded
182
+ - `AuthenticationError` — Invalid API key
183
+ - `NoResultsError` — No results for query
184
+
185
+ ### Utility Functions
186
+
187
+ - `validateCoordinates(lat, lng)` — Returns `{ valid, error? }`
188
+ - `isValidLatitude(lat)` / `isValidLongitude(lng)` — Bounds check
189
+ - `normalizeCountryCode(code)` — Normalize to ISO 3166-1 alpha-2
190
+ - `mapGooglePlaceType(types)` / `mapOSMPlaceType(type, addressType?)` — Map provider types to `Location['type']`
191
+
192
+ ## License
193
+
194
+ ISC
@@ -0,0 +1,342 @@
1
+ import { Client } from "@googlemaps/google-maps-services-js";
2
+ import { getCache } from "@happyvertical/cache";
3
+ import { InvalidQueryError, AuthenticationError, RateLimitError, GeoError, mapGooglePlaceType, normalizeCountryCode, validateCoordinates } from "../index.js";
4
+ class GoogleMapsProvider {
5
+ client;
6
+ apiKey;
7
+ timeout;
8
+ maxResults;
9
+ cache = null;
10
+ constructor(options) {
11
+ this.client = new Client({});
12
+ this.apiKey = options.apiKey;
13
+ this.timeout = options.timeout || 1e4;
14
+ this.maxResults = options.maxResults || 10;
15
+ this.initCache();
16
+ }
17
+ /**
18
+ * Initializes the memory cache for geocoding results
19
+ */
20
+ async initCache() {
21
+ try {
22
+ this.cache = await getCache({
23
+ provider: "memory",
24
+ namespace: "geo:google",
25
+ defaultTTL: 86400,
26
+ // 24 hour cache for location data
27
+ maxSize: 20 * 1024 * 1024,
28
+ // 20MB
29
+ maxEntries: 5e3,
30
+ evictionPolicy: "lru"
31
+ });
32
+ } catch (error) {
33
+ console.warn("Failed to initialize geo cache:", error);
34
+ }
35
+ }
36
+ /**
37
+ * Generates a cache key for geocoding requests
38
+ */
39
+ getCacheKey(type, ...parts) {
40
+ return `${type}:${parts.join(":")}`;
41
+ }
42
+ /**
43
+ * Look up locations based on a query string
44
+ */
45
+ async lookup(query) {
46
+ if (!query || query.trim().length === 0) {
47
+ throw new InvalidQueryError(query, "google");
48
+ }
49
+ const cacheKey = this.getCacheKey("lookup", query, String(this.maxResults));
50
+ if (this.cache) {
51
+ const cached = await this.cache.get(cacheKey);
52
+ if (cached) {
53
+ return cached;
54
+ }
55
+ }
56
+ try {
57
+ const response = await this.client.geocode({
58
+ params: {
59
+ address: query,
60
+ key: this.apiKey
61
+ },
62
+ timeout: this.timeout
63
+ });
64
+ if (response.data.status === "ZERO_RESULTS") {
65
+ return [];
66
+ }
67
+ if (response.data.status === "REQUEST_DENIED") {
68
+ throw new AuthenticationError("google");
69
+ }
70
+ if (response.data.status === "OVER_QUERY_LIMIT") {
71
+ throw new RateLimitError("google");
72
+ }
73
+ if (response.data.status !== "OK") {
74
+ throw new GeoError(
75
+ `Google Maps API error: ${response.data.status}`,
76
+ "API_ERROR",
77
+ "google"
78
+ );
79
+ }
80
+ const results = response.data.results.slice(0, this.maxResults);
81
+ const locations = results.map(
82
+ (result) => this.mapGoogleResultToLocation(result)
83
+ );
84
+ if (this.cache) {
85
+ await this.cache.set(cacheKey, locations);
86
+ }
87
+ return locations;
88
+ } catch (error) {
89
+ if (error instanceof GeoError) {
90
+ throw error;
91
+ }
92
+ throw new GeoError(
93
+ `Failed to lookup location: ${error.message}`,
94
+ "LOOKUP_FAILED",
95
+ "google"
96
+ );
97
+ }
98
+ }
99
+ /**
100
+ * Reverse geocode from coordinates to location
101
+ */
102
+ async reverseGeocode(latitude, longitude) {
103
+ const validation = validateCoordinates(latitude, longitude);
104
+ if (!validation.valid) {
105
+ throw new InvalidQueryError(
106
+ `${latitude}, ${longitude}: ${validation.error}`,
107
+ "google"
108
+ );
109
+ }
110
+ const cacheKey = this.getCacheKey(
111
+ "reverse",
112
+ String(latitude),
113
+ String(longitude),
114
+ String(this.maxResults)
115
+ );
116
+ if (this.cache) {
117
+ const cached = await this.cache.get(cacheKey);
118
+ if (cached) {
119
+ return cached;
120
+ }
121
+ }
122
+ try {
123
+ const response = await this.client.reverseGeocode({
124
+ params: {
125
+ latlng: { lat: latitude, lng: longitude },
126
+ key: this.apiKey
127
+ },
128
+ timeout: this.timeout
129
+ });
130
+ if (response.data.status === "ZERO_RESULTS") {
131
+ return [];
132
+ }
133
+ if (response.data.status === "REQUEST_DENIED") {
134
+ throw new AuthenticationError("google");
135
+ }
136
+ if (response.data.status === "OVER_QUERY_LIMIT") {
137
+ throw new RateLimitError("google");
138
+ }
139
+ if (response.data.status !== "OK") {
140
+ throw new GeoError(
141
+ `Google Maps API error: ${response.data.status}`,
142
+ "API_ERROR",
143
+ "google"
144
+ );
145
+ }
146
+ const results = response.data.results.slice(0, this.maxResults);
147
+ const locations = results.map(
148
+ (result) => this.mapGoogleResultToLocation(result)
149
+ );
150
+ if (this.cache) {
151
+ await this.cache.set(cacheKey, locations);
152
+ }
153
+ return locations;
154
+ } catch (error) {
155
+ if (error instanceof GeoError) {
156
+ throw error;
157
+ }
158
+ throw new GeoError(
159
+ `Failed to reverse geocode: ${error.message}`,
160
+ "REVERSE_GEOCODE_FAILED",
161
+ "google"
162
+ );
163
+ }
164
+ }
165
+ /**
166
+ * Find POIs near a coordinate using the Places API Nearby Search endpoint.
167
+ *
168
+ * Maps to `placesNearby` on the Google Maps Services SDK. Note that Places
169
+ * Nearby Search is a separate product line from Geocoding: it requires the
170
+ * Places API to be enabled for the supplied API key and is billed per
171
+ * request. Results are deduped across multi-type requests by `place_id`.
172
+ */
173
+ async findPoisNear(latitude, longitude, radiusMeters, options = {}) {
174
+ const validation = validateCoordinates(latitude, longitude);
175
+ if (!validation.valid) {
176
+ throw new InvalidQueryError(
177
+ `${latitude}, ${longitude}: ${validation.error}`,
178
+ "google"
179
+ );
180
+ }
181
+ if (!(radiusMeters > 0) || radiusMeters > 5e4) {
182
+ throw new InvalidQueryError(
183
+ `radius ${radiusMeters}m must be in (0, 50000]`,
184
+ "google"
185
+ );
186
+ }
187
+ const limit = options.limit ?? this.maxResults;
188
+ if (!Number.isInteger(limit) || limit < 1) {
189
+ throw new InvalidQueryError(
190
+ `limit ${limit} must be a positive integer`,
191
+ "google"
192
+ );
193
+ }
194
+ const types = options.types && options.types.length > 0 ? options.types : [void 0];
195
+ const cacheKey = this.getCacheKey(
196
+ "pois",
197
+ String(latitude),
198
+ String(longitude),
199
+ String(radiusMeters),
200
+ (options.types ?? []).join(","),
201
+ options.keyword ?? "",
202
+ options.language ?? "",
203
+ String(limit)
204
+ );
205
+ if (this.cache) {
206
+ const cached = await this.cache.get(cacheKey);
207
+ if (cached) return cached;
208
+ }
209
+ try {
210
+ const merged = /* @__PURE__ */ new Map();
211
+ for (const type of types) {
212
+ let pageToken;
213
+ let pagesFetched = 0;
214
+ const maxPagesPerType = 3;
215
+ while (pagesFetched < maxPagesPerType) {
216
+ const params = pageToken ? { pagetoken: pageToken, key: this.apiKey } : {
217
+ location: { lat: latitude, lng: longitude },
218
+ radius: radiusMeters,
219
+ ...type ? { type } : {},
220
+ ...options.keyword ? { keyword: options.keyword } : {},
221
+ ...options.language ? { language: options.language } : {},
222
+ key: this.apiKey
223
+ };
224
+ const response = await this.client.placesNearby({
225
+ params,
226
+ timeout: this.timeout
227
+ });
228
+ if (response.data.status === "ZERO_RESULTS") break;
229
+ if (response.data.status === "REQUEST_DENIED") {
230
+ throw new AuthenticationError("google");
231
+ }
232
+ if (response.data.status === "OVER_QUERY_LIMIT") {
233
+ throw new RateLimitError("google");
234
+ }
235
+ if (response.data.status === "INVALID_REQUEST" && pageToken) {
236
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
237
+ continue;
238
+ }
239
+ if (response.data.status !== "OK") {
240
+ throw new GeoError(
241
+ `Google Places API error: ${response.data.status}`,
242
+ "API_ERROR",
243
+ "google"
244
+ );
245
+ }
246
+ for (const result of response.data.results) {
247
+ const location = this.mapGooglePoiToLocation(result);
248
+ if (!merged.has(location.id)) {
249
+ merged.set(location.id, location);
250
+ }
251
+ if (merged.size >= limit) break;
252
+ }
253
+ pagesFetched += 1;
254
+ if (merged.size >= limit) break;
255
+ pageToken = response.data.next_page_token;
256
+ if (!pageToken) break;
257
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
258
+ }
259
+ if (merged.size >= limit) break;
260
+ }
261
+ const locations = [...merged.values()];
262
+ if (this.cache) await this.cache.set(cacheKey, locations);
263
+ return locations;
264
+ } catch (error) {
265
+ if (error instanceof GeoError) throw error;
266
+ throw new GeoError(
267
+ `Failed to find POIs: ${error.message}`,
268
+ "POI_SEARCH_FAILED",
269
+ "google"
270
+ );
271
+ }
272
+ }
273
+ /**
274
+ * Map Google Places Nearby Search result to standardized Location. The
275
+ * Places schema is a superset of Geocoding (adds `name`, `vicinity`,
276
+ * `types[]` semantics slightly different from geocoding `types`) so this
277
+ * is a separate mapper from `mapGoogleResultToLocation`.
278
+ */
279
+ mapGooglePoiToLocation(result) {
280
+ const loc = result.geometry?.location;
281
+ const latitude = loc?.lat ?? 0;
282
+ const longitude = loc?.lng ?? 0;
283
+ const name = result.name ? result.vicinity ? `${result.name}, ${result.vicinity}` : result.name : result.vicinity || "Unknown place";
284
+ return {
285
+ id: result.place_id,
286
+ type: "point_of_interest",
287
+ name,
288
+ latitude,
289
+ longitude,
290
+ addressComponents: {},
291
+ countryCode: "XX",
292
+ raw: result
293
+ };
294
+ }
295
+ /**
296
+ * Map Google Geocoding API result to standardized Location
297
+ */
298
+ mapGoogleResultToLocation(result) {
299
+ const addressComponents = {};
300
+ let countryCode = "XX";
301
+ for (const component of result.address_components || []) {
302
+ const types = component.types;
303
+ if (types.includes("street_number")) {
304
+ addressComponents.streetNumber = component.long_name;
305
+ }
306
+ if (types.includes("route")) {
307
+ addressComponents.streetName = component.long_name;
308
+ }
309
+ if (types.includes("locality")) {
310
+ addressComponents.city = component.long_name;
311
+ }
312
+ if (types.includes("administrative_area_level_1")) {
313
+ addressComponents.region = component.long_name;
314
+ }
315
+ if (types.includes("country")) {
316
+ addressComponents.country = component.long_name;
317
+ countryCode = component.short_name;
318
+ }
319
+ if (types.includes("postal_code")) {
320
+ addressComponents.postalCode = component.long_name;
321
+ }
322
+ }
323
+ const location = result.geometry?.location;
324
+ const latitude = location?.lat ?? 0;
325
+ const longitude = location?.lng ?? 0;
326
+ const type = mapGooglePlaceType(result.types || []);
327
+ return {
328
+ id: result.place_id,
329
+ type,
330
+ name: result.formatted_address,
331
+ latitude,
332
+ longitude,
333
+ addressComponents,
334
+ countryCode: normalizeCountryCode(countryCode),
335
+ raw: result
336
+ };
337
+ }
338
+ }
339
+ export {
340
+ GoogleMapsProvider
341
+ };
342
+ //# sourceMappingURL=google-Ci3_ec7t.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-Ci3_ec7t.js","sources":["../../src/providers/google.ts"],"sourcesContent":["/**\n * Google Maps provider implementation\n */\n\nimport { Client } from '@googlemaps/google-maps-services-js';\nimport type { CacheAdapter } from '@happyvertical/cache';\nimport { getCache } from '@happyvertical/cache';\nimport type {\n GeoProvider,\n GoogleMapsOptions,\n Location,\n PoiSearchOptions,\n} from '../shared/types';\nimport {\n AuthenticationError,\n GeoError,\n InvalidQueryError,\n RateLimitError,\n} from '../shared/types';\nimport {\n mapGooglePlaceType,\n normalizeCountryCode,\n validateCoordinates,\n} from '../shared/utils';\n\n/**\n * Google Maps provider implementation with in-memory caching\n */\nexport class GoogleMapsProvider implements GeoProvider {\n private client: Client;\n private apiKey: string;\n private timeout: number;\n private maxResults: number;\n private cache: CacheAdapter | null = null;\n\n constructor(options: GoogleMapsOptions) {\n this.client = new Client({});\n this.apiKey = options.apiKey;\n this.timeout = options.timeout || 10000;\n this.maxResults = options.maxResults || 10;\n\n // Initialize memory cache asynchronously\n this.initCache();\n }\n\n /**\n * Initializes the memory cache for geocoding results\n */\n private async initCache(): Promise<void> {\n try {\n this.cache = await getCache({\n provider: 'memory',\n namespace: 'geo:google',\n defaultTTL: 86400, // 24 hour cache for location data\n maxSize: 20 * 1024 * 1024, // 20MB\n maxEntries: 5000,\n evictionPolicy: 'lru',\n });\n } catch (error) {\n // Cache initialization failure shouldn't break the provider\n console.warn('Failed to initialize geo cache:', error);\n }\n }\n\n /**\n * Generates a cache key for geocoding requests\n */\n private getCacheKey(type: string, ...parts: string[]): string {\n return `${type}:${parts.join(':')}`;\n }\n\n /**\n * Look up locations based on a query string\n */\n async lookup(query: string): Promise<Location[]> {\n if (!query || query.trim().length === 0) {\n throw new InvalidQueryError(query, 'google');\n }\n\n // Check cache first\n const cacheKey = this.getCacheKey('lookup', query, String(this.maxResults));\n if (this.cache) {\n const cached = await this.cache.get<Location[]>(cacheKey);\n if (cached) {\n return cached;\n }\n }\n\n try {\n const response = await this.client.geocode({\n params: {\n address: query,\n key: this.apiKey,\n },\n timeout: this.timeout,\n });\n\n if (response.data.status === 'ZERO_RESULTS') {\n return [];\n }\n\n if (response.data.status === 'REQUEST_DENIED') {\n throw new AuthenticationError('google');\n }\n\n if (response.data.status === 'OVER_QUERY_LIMIT') {\n throw new RateLimitError('google');\n }\n\n if (response.data.status !== 'OK') {\n throw new GeoError(\n `Google Maps API error: ${response.data.status}`,\n 'API_ERROR',\n 'google',\n );\n }\n\n const results = response.data.results.slice(0, this.maxResults);\n const locations = results.map((result) =>\n this.mapGoogleResultToLocation(result),\n );\n\n // Cache the result\n if (this.cache) {\n await this.cache.set(cacheKey, locations);\n }\n\n return locations;\n } catch (error) {\n if (error instanceof GeoError) {\n throw error;\n }\n\n throw new GeoError(\n `Failed to lookup location: ${(error as Error).message}`,\n 'LOOKUP_FAILED',\n 'google',\n );\n }\n }\n\n /**\n * Reverse geocode from coordinates to location\n */\n async reverseGeocode(\n latitude: number,\n longitude: number,\n ): Promise<Location[]> {\n const validation = validateCoordinates(latitude, longitude);\n if (!validation.valid) {\n throw new InvalidQueryError(\n `${latitude}, ${longitude}: ${validation.error}`,\n 'google',\n );\n }\n\n // Check cache first\n const cacheKey = this.getCacheKey(\n 'reverse',\n String(latitude),\n String(longitude),\n String(this.maxResults),\n );\n if (this.cache) {\n const cached = await this.cache.get<Location[]>(cacheKey);\n if (cached) {\n return cached;\n }\n }\n\n try {\n const response = await this.client.reverseGeocode({\n params: {\n latlng: { lat: latitude, lng: longitude },\n key: this.apiKey,\n },\n timeout: this.timeout,\n });\n\n if (response.data.status === 'ZERO_RESULTS') {\n return [];\n }\n\n if (response.data.status === 'REQUEST_DENIED') {\n throw new AuthenticationError('google');\n }\n\n if (response.data.status === 'OVER_QUERY_LIMIT') {\n throw new RateLimitError('google');\n }\n\n if (response.data.status !== 'OK') {\n throw new GeoError(\n `Google Maps API error: ${response.data.status}`,\n 'API_ERROR',\n 'google',\n );\n }\n\n const results = response.data.results.slice(0, this.maxResults);\n const locations = results.map((result) =>\n this.mapGoogleResultToLocation(result),\n );\n\n // Cache the result\n if (this.cache) {\n await this.cache.set(cacheKey, locations);\n }\n\n return locations;\n } catch (error) {\n if (error instanceof GeoError) {\n throw error;\n }\n\n throw new GeoError(\n `Failed to reverse geocode: ${(error as Error).message}`,\n 'REVERSE_GEOCODE_FAILED',\n 'google',\n );\n }\n }\n\n /**\n * Find POIs near a coordinate using the Places API Nearby Search endpoint.\n *\n * Maps to `placesNearby` on the Google Maps Services SDK. Note that Places\n * Nearby Search is a separate product line from Geocoding: it requires the\n * Places API to be enabled for the supplied API key and is billed per\n * request. Results are deduped across multi-type requests by `place_id`.\n */\n async findPoisNear(\n latitude: number,\n longitude: number,\n radiusMeters: number,\n options: PoiSearchOptions = {},\n ): Promise<Location[]> {\n const validation = validateCoordinates(latitude, longitude);\n if (!validation.valid) {\n throw new InvalidQueryError(\n `${latitude}, ${longitude}: ${validation.error}`,\n 'google',\n );\n }\n if (!(radiusMeters > 0) || radiusMeters > 50_000) {\n throw new InvalidQueryError(\n `radius ${radiusMeters}m must be in (0, 50000]`,\n 'google',\n );\n }\n\n const limit = options.limit ?? this.maxResults;\n if (!Number.isInteger(limit) || limit < 1) {\n throw new InvalidQueryError(\n `limit ${limit} must be a positive integer`,\n 'google',\n );\n }\n const types =\n options.types && options.types.length > 0 ? options.types : [undefined];\n\n const cacheKey = this.getCacheKey(\n 'pois',\n String(latitude),\n String(longitude),\n String(radiusMeters),\n (options.types ?? []).join(','),\n options.keyword ?? '',\n options.language ?? '',\n String(limit),\n );\n if (this.cache) {\n const cached = await this.cache.get<Location[]>(cacheKey);\n if (cached) return cached;\n }\n\n try {\n const merged = new Map<string, Location>();\n for (const type of types) {\n // Places Nearby returns up to 20 results per call and up to 60 total\n // via `next_page_token`, with Google requiring a short activation\n // delay after each token is issued. Keep paging per-type until we\n // either satisfy the caller's `limit`, hit the 3-page ceiling, or\n // run out of tokens.\n let pageToken: string | undefined;\n let pagesFetched = 0;\n const maxPagesPerType = 3;\n\n while (pagesFetched < maxPagesPerType) {\n const params = pageToken\n ? { pagetoken: pageToken, key: this.apiKey }\n : {\n location: { lat: latitude, lng: longitude },\n radius: radiusMeters,\n ...(type ? { type } : {}),\n ...(options.keyword ? { keyword: options.keyword } : {}),\n ...(options.language\n ? { language: options.language as any }\n : {}),\n key: this.apiKey,\n };\n\n const response = await this.client.placesNearby({\n params,\n timeout: this.timeout,\n });\n\n if (response.data.status === 'ZERO_RESULTS') break;\n if (response.data.status === 'REQUEST_DENIED') {\n throw new AuthenticationError('google');\n }\n if (response.data.status === 'OVER_QUERY_LIMIT') {\n throw new RateLimitError('google');\n }\n if (response.data.status === 'INVALID_REQUEST' && pageToken) {\n // Google returns INVALID_REQUEST if the page token is polled\n // before it has activated (typically ≤2s). Back off and retry\n // one more time rather than aborting the whole call.\n await new Promise((resolve) => setTimeout(resolve, 2000));\n continue;\n }\n if (response.data.status !== 'OK') {\n throw new GeoError(\n `Google Places API error: ${response.data.status}`,\n 'API_ERROR',\n 'google',\n );\n }\n\n for (const result of response.data.results) {\n const location = this.mapGooglePoiToLocation(result);\n if (!merged.has(location.id)) {\n merged.set(location.id, location);\n }\n if (merged.size >= limit) break;\n }\n\n pagesFetched += 1;\n if (merged.size >= limit) break;\n\n pageToken = response.data.next_page_token;\n if (!pageToken) break;\n // `pagetoken` isn't activated instantly; Google's docs say to\n // wait a short moment before using it. 2s is the commonly-cited\n // activation window.\n await new Promise((resolve) => setTimeout(resolve, 2000));\n }\n\n if (merged.size >= limit) break;\n }\n\n const locations = [...merged.values()];\n if (this.cache) await this.cache.set(cacheKey, locations);\n return locations;\n } catch (error) {\n if (error instanceof GeoError) throw error;\n throw new GeoError(\n `Failed to find POIs: ${(error as Error).message}`,\n 'POI_SEARCH_FAILED',\n 'google',\n );\n }\n }\n\n /**\n * Map Google Places Nearby Search result to standardized Location. The\n * Places schema is a superset of Geocoding (adds `name`, `vicinity`,\n * `types[]` semantics slightly different from geocoding `types`) so this\n * is a separate mapper from `mapGoogleResultToLocation`.\n */\n private mapGooglePoiToLocation(result: any): Location {\n const loc = result.geometry?.location;\n const latitude = loc?.lat ?? 0;\n const longitude = loc?.lng ?? 0;\n const name = result.name\n ? result.vicinity\n ? `${result.name}, ${result.vicinity}`\n : result.name\n : result.vicinity || 'Unknown place';\n\n return {\n id: result.place_id,\n type: 'point_of_interest',\n name,\n latitude,\n longitude,\n addressComponents: {},\n countryCode: 'XX',\n raw: result,\n };\n }\n\n /**\n * Map Google Geocoding API result to standardized Location\n */\n private mapGoogleResultToLocation(result: any): Location {\n // Extract address components\n const addressComponents: Location['addressComponents'] = {};\n let countryCode = 'XX';\n\n for (const component of result.address_components || []) {\n const types = component.types;\n\n if (types.includes('street_number')) {\n addressComponents.streetNumber = component.long_name;\n }\n if (types.includes('route')) {\n addressComponents.streetName = component.long_name;\n }\n if (types.includes('locality')) {\n addressComponents.city = component.long_name;\n }\n if (types.includes('administrative_area_level_1')) {\n addressComponents.region = component.long_name;\n }\n if (types.includes('country')) {\n addressComponents.country = component.long_name;\n countryCode = component.short_name;\n }\n if (types.includes('postal_code')) {\n addressComponents.postalCode = component.long_name;\n }\n }\n\n // Extract coordinates\n const location = result.geometry?.location;\n const latitude = location?.lat ?? 0;\n const longitude = location?.lng ?? 0;\n\n // Determine location type\n const type = mapGooglePlaceType(result.types || []);\n\n return {\n id: result.place_id,\n type,\n name: result.formatted_address,\n latitude,\n longitude,\n addressComponents,\n countryCode: normalizeCountryCode(countryCode),\n raw: result,\n };\n }\n}\n"],"names":[],"mappings":"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"}