@happyvertical/geo 0.96.1 → 0.98.0

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 CHANGED
@@ -27,7 +27,7 @@ pnpm --filter @happyvertical/geo clean
27
27
  ## Ecosystem Relationships
28
28
  - Provides: Standardized geographical information interface supporting Google Maps and OpenStreetMap
29
29
  - Implements: none
30
- - Requires: @happyvertical/cache, @happyvertical/utils, @googlemaps/google-maps-services-js
30
+ - Requires: @happyvertical/cache, @happyvertical/utils, @googlemaps/google-maps-services-js, @photostructure/tz-lookup
31
31
  - Stability: stable (Primary package surface is described as implemented and production-oriented.)
32
32
  <!-- END AGENT:GENERATED -->
33
33
 
package/README.md CHANGED
@@ -135,6 +135,24 @@ Factory function returning a geocoding adapter. Options are a discriminated unio
135
135
  | `maxResults` | optional | optional | Max results (default: 10) |
136
136
  | `userAgent` | — | optional | Custom User-Agent for Nominatim |
137
137
  | `rateLimitDelay` | — | optional | Delay between requests ms (default: 1000) |
138
+ | `timezoneLookup` | `'offline'` \| `'api'` \| `'none'` | `'offline'` \| `'none'` | How `Location.timezone` is filled (default `'offline'`; env `HAVE_GEO_TIMEZONE_LOOKUP`) |
139
+
140
+ #### Time zones
141
+
142
+ Every result carries an IANA `timezone` (for example `'America/Edmonton'`)
143
+ derived from its coordinates. The default `'offline'` mode uses the bundled
144
+ [`@photostructure/tz-lookup`](https://github.com/photostructure/tz-lookup)
145
+ table (CC0-1.0, ~90 KB, no network requests); it can pick a neighbouring zone
146
+ within a few kilometres of a zone border. With the Google provider,
147
+ `timezoneLookup: 'api'` asks the Google Time Zone API instead (enable it for
148
+ the key; billed per request; one call per distinct coordinate; returns CLDR
149
+ canonical IDs such as `Asia/Calcutta`; distinct coordinates are looked up
150
+ concurrently) and falls back to the offline table if the call fails, logging
151
+ one warning per adapter. `'none'` leaves `timezone` unset. Values are
152
+ case-insensitive; any other value makes `getGeoAdapter` throw a `GeoError`
153
+ (`INVALID_OPTION`). OpenStreetMap has no time zone API, so `'api'` there logs
154
+ one warning and uses `'offline'`. The offline lookup is also
155
+ exported as `timezoneForCoordinates(latitude, longitude)`.
138
156
 
139
157
  ### `GeoAdapter` Interface
140
158
 
@@ -184,6 +202,7 @@ All extend `GeoError`:
184
202
 
185
203
  ### Utility Functions
186
204
 
205
+ - `timezoneForCoordinates(lat, lng)` — Offline IANA time zone for a coordinate (`undefined` if invalid)
187
206
  - `validateCoordinates(lat, lng)` — Returns `{ valid, error? }`
188
207
  - `isValidLatitude(lat)` / `isValidLongitude(lng)` — Bounds check
189
208
  - `normalizeCountryCode(code)` — Normalize to ISO 3166-1 alpha-2
@@ -1,4 +1,5 @@
1
- import { AuthenticationError, GeoError, InvalidQueryError, RateLimitError, mapGooglePlaceType, normalizeCountryCode, validateCoordinates } from "../index.js";
1
+ import { AuthenticationError, GeoError, InvalidQueryError, RateLimitError, mapGooglePlaceType, normalizeCountryCode, timezoneForCoordinates, validateCoordinates } from "../index.js";
2
+ import { t as resolveTimezoneLookup } from "./timezone-mode-DFovZtFK.js";
2
3
  import { Client } from "@googlemaps/google-maps-services-js";
3
4
  import { getCache } from "@happyvertical/cache";
4
5
  //#region src/providers/google.ts
@@ -14,11 +15,14 @@ var GoogleMapsProvider = class {
14
15
  timeout;
15
16
  maxResults;
16
17
  cache = null;
18
+ timezoneLookup;
19
+ warnedTimezoneFallback = false;
17
20
  constructor(options) {
18
21
  this.client = new Client({});
19
22
  this.apiKey = options.apiKey;
20
23
  this.timeout = options.timeout || 1e4;
21
24
  this.maxResults = options.maxResults || 10;
25
+ this.timezoneLookup = resolveTimezoneLookup(options.timezoneLookup, "google");
22
26
  this.initCache();
23
27
  }
24
28
  /**
@@ -39,10 +43,72 @@ var GoogleMapsProvider = class {
39
43
  }
40
44
  }
41
45
  /**
42
- * Generates a cache key for geocoding requests
46
+ * Generates a cache key for geocoding requests. The time zone mode is part
47
+ * of the key so adapters with different modes never share results.
43
48
  */
44
49
  getCacheKey(type, ...parts) {
45
- return `${type}:${parts.join(":")}`;
50
+ return `${type}:${this.timezoneLookup}:${parts.join(":")}`;
51
+ }
52
+ /**
53
+ * Fills `Location.timezone` according to the configured mode. In `'api'`
54
+ * mode the Google Time Zone API is called once per distinct coordinate,
55
+ * concurrently; a failed or non-OK call falls back to the offline table
56
+ * for that point.
57
+ */
58
+ async applyTimezones(locations) {
59
+ if (this.timezoneLookup === "none") return locations;
60
+ const byCoordinate = /* @__PURE__ */ new Map();
61
+ const pending = locations.map((location) => {
62
+ if (location.timezone) return Promise.resolve(location.timezone);
63
+ const key = `${location.latitude},${location.longitude}`;
64
+ let lookup = byCoordinate.get(key);
65
+ if (!lookup) {
66
+ lookup = this.timezoneLookup === "api" ? this.fetchTimezone(location.latitude, location.longitude) : Promise.resolve(timezoneForCoordinates(location.latitude, location.longitude));
67
+ byCoordinate.set(key, lookup);
68
+ }
69
+ return lookup;
70
+ });
71
+ const timezones = await Promise.all(pending);
72
+ locations.forEach((location, index) => {
73
+ const timezone = timezones[index];
74
+ if (timezone) location.timezone = timezone;
75
+ });
76
+ return locations;
77
+ }
78
+ /**
79
+ * Resolves a coordinate's IANA zone through the Google Time Zone API,
80
+ * falling back to the offline table when the API is unavailable, denied,
81
+ * or returns no zone.
82
+ */
83
+ async fetchTimezone(latitude, longitude) {
84
+ try {
85
+ const response = await this.client.timezone({
86
+ params: {
87
+ location: {
88
+ lat: latitude,
89
+ lng: longitude
90
+ },
91
+ timestamp: Math.floor(Date.now() / 1e3),
92
+ key: this.apiKey
93
+ },
94
+ timeout: this.timeout
95
+ });
96
+ if (response.data.status === "OK" && response.data.timeZoneId) return response.data.timeZoneId;
97
+ this.warnTimezoneFallback(`status ${response.data.status}`);
98
+ } catch {
99
+ this.warnTimezoneFallback("request failed");
100
+ }
101
+ return timezoneForCoordinates(latitude, longitude);
102
+ }
103
+ /**
104
+ * Warns once per adapter that Time Zone API lookups are falling back to
105
+ * the offline table. Logs only a short reason, never the error, request,
106
+ * or key.
107
+ */
108
+ warnTimezoneFallback(reason) {
109
+ if (this.warnedTimezoneFallback) return;
110
+ this.warnedTimezoneFallback = true;
111
+ console.warn(`@happyvertical/geo: Google Time Zone API lookup failed (${reason}); using the offline time zone table. Enable the Time Zone API for this key or use timezoneLookup: 'offline'.`);
46
112
  }
47
113
  /**
48
114
  * Look up locations based on a query string
@@ -66,7 +132,8 @@ var GoogleMapsProvider = class {
66
132
  if (response.data.status === "REQUEST_DENIED") throw new AuthenticationError("google");
67
133
  if (response.data.status === "OVER_QUERY_LIMIT") throw new RateLimitError("google");
68
134
  if (response.data.status !== "OK") throw new GeoError(`Google Maps API error: ${response.data.status}`, "API_ERROR", "google");
69
- const locations = response.data.results.slice(0, this.maxResults).map((result) => this.mapGoogleResultToLocation(result));
135
+ const results = response.data.results.slice(0, this.maxResults);
136
+ const locations = await this.applyTimezones(results.map((result) => this.mapGoogleResultToLocation(result)));
70
137
  if (this.cache) await this.cache.set(cacheKey, locations);
71
138
  return locations;
72
139
  } catch (error) {
@@ -100,7 +167,8 @@ var GoogleMapsProvider = class {
100
167
  if (response.data.status === "REQUEST_DENIED") throw new AuthenticationError("google");
101
168
  if (response.data.status === "OVER_QUERY_LIMIT") throw new RateLimitError("google");
102
169
  if (response.data.status !== "OK") throw new GeoError(`Google Maps API error: ${response.data.status}`, "API_ERROR", "google");
103
- const locations = response.data.results.slice(0, this.maxResults).map((result) => this.mapGoogleResultToLocation(result));
170
+ const results = response.data.results.slice(0, this.maxResults);
171
+ const locations = await this.applyTimezones(results.map((result) => this.mapGoogleResultToLocation(result)));
104
172
  if (this.cache) await this.cache.set(cacheKey, locations);
105
173
  return locations;
106
174
  } catch (error) {
@@ -174,7 +242,7 @@ var GoogleMapsProvider = class {
174
242
  }
175
243
  if (merged.size >= limit) break;
176
244
  }
177
- const locations = [...merged.values()];
245
+ const locations = await this.applyTimezones([...merged.values()]);
178
246
  if (this.cache) await this.cache.set(cacheKey, locations);
179
247
  return locations;
180
248
  } catch (error) {
@@ -241,4 +309,4 @@ var GoogleMapsProvider = class {
241
309
  //#endregion
242
310
  export { GoogleMapsProvider };
243
311
 
244
- //# sourceMappingURL=google-CZUa1ho0.js.map
312
+ //# sourceMappingURL=google-Cb-ytnTf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-Cb-ytnTf.js","names":[],"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 { timezoneForCoordinates } from '../shared/timezone';\nimport { resolveTimezoneLookup } from '../shared/timezone-mode';\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 private timezoneLookup: 'offline' | 'api' | 'none';\n private warnedTimezoneFallback = false;\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 this.timezoneLookup = resolveTimezoneLookup(\n options.timezoneLookup,\n 'google',\n );\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. The time zone mode is part\n * of the key so adapters with different modes never share results.\n */\n private getCacheKey(type: string, ...parts: string[]): string {\n return `${type}:${this.timezoneLookup}:${parts.join(':')}`;\n }\n\n /**\n * Fills `Location.timezone` according to the configured mode. In `'api'`\n * mode the Google Time Zone API is called once per distinct coordinate,\n * concurrently; a failed or non-OK call falls back to the offline table\n * for that point.\n */\n private async applyTimezones(locations: Location[]): Promise<Location[]> {\n if (this.timezoneLookup === 'none') return locations;\n\n const byCoordinate = new Map<string, Promise<string | undefined>>();\n const pending = locations.map((location) => {\n if (location.timezone) return Promise.resolve(location.timezone);\n const key = `${location.latitude},${location.longitude}`;\n let lookup = byCoordinate.get(key);\n if (!lookup) {\n lookup =\n this.timezoneLookup === 'api'\n ? this.fetchTimezone(location.latitude, location.longitude)\n : Promise.resolve(\n timezoneForCoordinates(location.latitude, location.longitude),\n );\n byCoordinate.set(key, lookup);\n }\n return lookup;\n });\n\n const timezones = await Promise.all(pending);\n locations.forEach((location, index) => {\n const timezone = timezones[index];\n if (timezone) location.timezone = timezone;\n });\n return locations;\n }\n\n /**\n * Resolves a coordinate's IANA zone through the Google Time Zone API,\n * falling back to the offline table when the API is unavailable, denied,\n * or returns no zone.\n */\n private async fetchTimezone(\n latitude: number,\n longitude: number,\n ): Promise<string | undefined> {\n try {\n const response = await this.client.timezone({\n params: {\n location: { lat: latitude, lng: longitude },\n timestamp: Math.floor(Date.now() / 1000),\n key: this.apiKey,\n },\n timeout: this.timeout,\n });\n if (response.data.status === 'OK' && response.data.timeZoneId) {\n return response.data.timeZoneId;\n }\n this.warnTimezoneFallback(`status ${response.data.status}`);\n } catch {\n // Fall through to the offline table: a missing Time Zone API\n // entitlement must not fail an otherwise successful geocode. The error\n // is not logged: client errors can carry the request URL and key.\n this.warnTimezoneFallback('request failed');\n }\n return timezoneForCoordinates(latitude, longitude);\n }\n\n /**\n * Warns once per adapter that Time Zone API lookups are falling back to\n * the offline table. Logs only a short reason, never the error, request,\n * or key.\n */\n private warnTimezoneFallback(reason: string): void {\n if (this.warnedTimezoneFallback) return;\n this.warnedTimezoneFallback = true;\n console.warn(\n `@happyvertical/geo: Google Time Zone API lookup failed (${reason}); using the offline time zone table. Enable the Time Zone API for this key or use timezoneLookup: 'offline'.`,\n );\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 = await this.applyTimezones(\n results.map((result) => 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 = await this.applyTimezones(\n results.map((result) => 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 = await this.applyTimezones([...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"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- import { GeoError, InvalidQueryError, RateLimitError, mapOSMPlaceType, normalizeCountryCode, validateCoordinates } from "../index.js";
1
+ import { GeoError, InvalidQueryError, RateLimitError, mapOSMPlaceType, normalizeCountryCode, validateCoordinates, withOfflineTimezones } from "../index.js";
2
+ import { t as resolveTimezoneLookup } from "./timezone-mode-DFovZtFK.js";
2
3
  import { getCache } from "@happyvertical/cache";
3
4
  //#region src/providers/openstreetmap.ts
4
5
  /**
@@ -43,11 +44,13 @@ var OpenStreetMapProvider = class {
43
44
  timeout;
44
45
  maxResults;
45
46
  cache = null;
47
+ timezoneLookup;
46
48
  constructor(options) {
47
49
  this.userAgent = options.userAgent || "@happyvertical/geo (Node.js)";
48
50
  this.rateLimitDelay = options.rateLimitDelay || 1e3;
49
51
  this.timeout = options.timeout || 1e4;
50
52
  this.maxResults = options.maxResults || 10;
53
+ this.timezoneLookup = resolveTimezoneLookup(options.timezoneLookup, "openstreetmap");
51
54
  this.initCache();
52
55
  }
53
56
  /**
@@ -68,10 +71,17 @@ var OpenStreetMapProvider = class {
68
71
  }
69
72
  }
70
73
  /**
71
- * Generates a cache key for geocoding requests
74
+ * Generates a cache key for geocoding requests. The time zone mode is part
75
+ * of the key so adapters with different modes never share results.
72
76
  */
73
77
  getCacheKey(type, ...parts) {
74
- return `${type}:${parts.join(":")}`;
78
+ return `${type}:${this.timezoneLookup}:${parts.join(":")}`;
79
+ }
80
+ /**
81
+ * Fills `Location.timezone` according to the configured mode.
82
+ */
83
+ applyTimezones(locations) {
84
+ return this.timezoneLookup === "none" ? locations : withOfflineTimezones(locations);
75
85
  }
76
86
  /**
77
87
  * Enforce rate limiting by waiting if necessary
@@ -130,7 +140,8 @@ var OpenStreetMapProvider = class {
130
140
  if (cached) return cached;
131
141
  }
132
142
  try {
133
- const locations = (await this.fetchNominatim("search", { q: query })).map((result) => this.mapNominatimResultToLocation(result));
143
+ const results = await this.fetchNominatim("search", { q: query });
144
+ const locations = this.applyTimezones(results.map((result) => this.mapNominatimResultToLocation(result)));
134
145
  if (this.cache) await this.cache.set(cacheKey, locations);
135
146
  return locations;
136
147
  } catch (error) {
@@ -150,10 +161,11 @@ var OpenStreetMapProvider = class {
150
161
  if (cached) return cached;
151
162
  }
152
163
  try {
153
- const locations = (await this.fetchNominatim("reverse", {
164
+ const results = await this.fetchNominatim("reverse", {
154
165
  lat: latitude.toString(),
155
166
  lon: longitude.toString()
156
- })).map((result) => this.mapNominatimResultToLocation(result));
167
+ });
168
+ const locations = this.applyTimezones(results.map((result) => this.mapNominatimResultToLocation(result)));
157
169
  if (this.cache) await this.cache.set(cacheKey, locations);
158
170
  return locations;
159
171
  } catch (error) {
@@ -208,6 +220,7 @@ var OpenStreetMapProvider = class {
208
220
  locations.push(location);
209
221
  if (locations.length >= limit) break;
210
222
  }
223
+ this.applyTimezones(locations);
211
224
  if (this.cache) await this.cache.set(cacheKey, locations);
212
225
  return locations;
213
226
  } catch (error) {
@@ -311,4 +324,4 @@ var OpenStreetMapProvider = class {
311
324
  //#endregion
312
325
  export { OpenStreetMapProvider };
313
326
 
314
- //# sourceMappingURL=openstreetmap-St4Kw-vS.js.map
327
+ //# sourceMappingURL=openstreetmap-D55wPXMO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openstreetmap-D55wPXMO.js","names":[],"sources":["../../src/providers/openstreetmap.ts"],"sourcesContent":["/**\n * OpenStreetMap (Nominatim) provider implementation\n */\n\nimport type { CacheAdapter } from '@happyvertical/cache';\nimport { getCache } from '@happyvertical/cache';\nimport { withOfflineTimezones } from '../shared/timezone';\nimport { resolveTimezoneLookup } from '../shared/timezone-mode';\nimport type {\n GeoProvider,\n Location,\n OpenStreetMapOptions,\n PoiSearchOptions,\n} from '../shared/types';\nimport { GeoError, InvalidQueryError, RateLimitError } from '../shared/types';\nimport {\n mapOSMPlaceType,\n normalizeCountryCode,\n validateCoordinates,\n} from '../shared/utils';\n\n/**\n * OpenStreetMap Nominatim API response structure\n */\ninterface NominatimResult {\n place_id: number;\n licence: string;\n osm_type: string;\n osm_id: number;\n lat: string;\n lon: string;\n display_name: string;\n address?: {\n house_number?: string;\n road?: string;\n city?: string;\n town?: string;\n village?: string;\n state?: string;\n country?: string;\n postcode?: string;\n country_code?: string;\n };\n type?: string;\n addresstype?: string;\n boundingbox?: string[];\n [key: string]: any;\n}\n\n/**\n * Overpass API response element (node or way within the result set).\n */\ninterface OverpassElement {\n type: 'node' | 'way' | 'relation';\n id: number;\n lat?: number;\n lon?: number;\n center?: { lat: number; lon: number };\n tags?: Record<string, string>;\n}\n\n/**\n * Escape a literal string for safe interpolation into an Overpass\n * `name~\"...\"` regex match. Overpass uses POSIX extended regular\n * expressions; we escape the ERE metacharacters plus the enclosing\n * double-quote and backslash so callers can pass arbitrary user-entered\n * keywords without worrying about regex injection or accidental pattern\n * matching.\n */\nfunction escapeOverpassRegex(value: string): string {\n return value.replace(/[\"\\\\.*+?^${}()|[\\]]/g, '\\\\$&');\n}\n\n/**\n * Tag keys Overpass treats as POI-like. Used when the caller doesn't\n * specify `types` — broad enough to cover businesses, landmarks, and\n * amenities without pulling back every single residential address.\n */\nconst POI_TAG_KEYS = [\n 'amenity',\n 'shop',\n 'tourism',\n 'leisure',\n 'office',\n 'historic',\n 'craft',\n];\n\n/**\n * OpenStreetMap provider using Nominatim API with in-memory caching\n */\nexport class OpenStreetMapProvider implements GeoProvider {\n private baseUrl = 'https://nominatim.openstreetmap.org';\n /**\n * Overpass endpoint for POI search. The public instance has a community\n * use-policy similar to Nominatim's — be polite, cache aggressively, and\n * consider a self-hosted instance if you'd hit it hard.\n */\n private overpassUrl = 'https://overpass-api.de/api/interpreter';\n private userAgent: string;\n private rateLimitDelay: number;\n private lastRequestTime = 0;\n private timeout: number;\n private maxResults: number;\n private cache: CacheAdapter | null = null;\n private timezoneLookup: 'offline' | 'none';\n\n constructor(options: OpenStreetMapOptions) {\n this.userAgent = options.userAgent || '@happyvertical/geo (Node.js)';\n this.rateLimitDelay = options.rateLimitDelay || 1000; // 1 second default\n this.timeout = options.timeout || 10000;\n this.maxResults = options.maxResults || 10;\n this.timezoneLookup = resolveTimezoneLookup(\n options.timezoneLookup,\n 'openstreetmap',\n );\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:osm',\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. The time zone mode is part\n * of the key so adapters with different modes never share results.\n */\n private getCacheKey(type: string, ...parts: string[]): string {\n return `${type}:${this.timezoneLookup}:${parts.join(':')}`;\n }\n\n /**\n * Fills `Location.timezone` according to the configured mode.\n */\n private applyTimezones(locations: Location[]): Location[] {\n return this.timezoneLookup === 'none'\n ? locations\n : withOfflineTimezones(locations);\n }\n\n /**\n * Enforce rate limiting by waiting if necessary\n */\n private async enforceRateLimit(): Promise<void> {\n const now = Date.now();\n const timeSinceLastRequest = now - this.lastRequestTime;\n\n if (timeSinceLastRequest < this.rateLimitDelay) {\n const waitTime = this.rateLimitDelay - timeSinceLastRequest;\n await new Promise((resolve) => setTimeout(resolve, waitTime));\n }\n\n this.lastRequestTime = Date.now();\n }\n\n /**\n * Make HTTP request to Nominatim API\n */\n private async fetchNominatim(\n endpoint: string,\n params: Record<string, string>,\n ): Promise<NominatimResult[]> {\n await this.enforceRateLimit();\n\n const queryParams = new URLSearchParams({\n ...params,\n format: 'json',\n addressdetails: '1',\n limit: this.maxResults.toString(),\n });\n\n const url = `${this.baseUrl}/${endpoint}?${queryParams.toString()}`;\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n headers: {\n 'User-Agent': this.userAgent,\n Accept: 'application/json',\n },\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (response.status === 429) {\n throw new RateLimitError('openstreetmap');\n }\n\n if (!response.ok) {\n throw new GeoError(\n `Nominatim API error: ${response.status} ${response.statusText}`,\n 'API_ERROR',\n 'openstreetmap',\n );\n }\n\n const data = await response.json();\n // Nominatim search returns array, reverse returns single object\n if (Array.isArray(data)) {\n return data;\n }\n return data ? [data as NominatimResult] : [];\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof GeoError) {\n throw error;\n }\n\n if ((error as Error).name === 'AbortError') {\n throw new GeoError('Request timeout', 'TIMEOUT', 'openstreetmap');\n }\n\n throw new GeoError(\n `Failed to fetch from Nominatim: ${(error as Error).message}`,\n 'FETCH_FAILED',\n 'openstreetmap',\n );\n }\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, 'openstreetmap');\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 results = await this.fetchNominatim('search', { q: query });\n const locations = this.applyTimezones(\n results.map((result) => this.mapNominatimResultToLocation(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 'openstreetmap',\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 'openstreetmap',\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 results = await this.fetchNominatim('reverse', {\n lat: latitude.toString(),\n lon: longitude.toString(),\n });\n\n // Nominatim reverse endpoint returns a single result or empty\n const locations = this.applyTimezones(\n results.map((result) => this.mapNominatimResultToLocation(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 'openstreetmap',\n );\n }\n }\n\n /**\n * Find POIs near a coordinate using the public Overpass API. Nominatim\n * only does address-level reverse geocoding — Overpass is the right tool\n * when you want \"every café within 200m\" kind of queries.\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 'openstreetmap',\n );\n }\n if (!(radiusMeters > 0)) {\n throw new InvalidQueryError(\n `radius ${radiusMeters}m must be > 0`,\n 'openstreetmap',\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 'openstreetmap',\n );\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 String(limit),\n );\n if (this.cache) {\n const cached = await this.cache.get<Location[]>(cacheKey);\n if (cached) return cached;\n }\n\n await this.enforceRateLimit();\n\n const query = this.buildOverpassQuery(\n latitude,\n longitude,\n radiusMeters,\n options,\n );\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(this.overpassUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'User-Agent': this.userAgent,\n Accept: 'application/json',\n },\n body: `data=${encodeURIComponent(query)}`,\n signal: controller.signal,\n });\n clearTimeout(timeoutId);\n\n if (response.status === 429) {\n throw new RateLimitError('openstreetmap');\n }\n if (!response.ok) {\n throw new GeoError(\n `Overpass API error: ${response.status} ${response.statusText}`,\n 'API_ERROR',\n 'openstreetmap',\n );\n }\n\n const data = (await response.json()) as { elements?: OverpassElement[] };\n const elements = Array.isArray(data.elements) ? data.elements : [];\n\n // Overpass returns each POI once per matched tag key, but when we\n // union multiple keys in the query elements can repeat — dedupe by\n // `(type, id)` so \"node/42\" only appears once regardless of how many\n // tag clauses matched.\n const seen = new Set<string>();\n const locations: Location[] = [];\n for (const element of elements) {\n const key = `${element.type}/${element.id}`;\n if (seen.has(key)) continue;\n seen.add(key);\n const location = this.mapOverpassElementToLocation(element);\n if (!location) continue;\n locations.push(location);\n if (locations.length >= limit) break;\n }\n\n this.applyTimezones(locations);\n if (this.cache) await this.cache.set(cacheKey, locations);\n return locations;\n } catch (error) {\n clearTimeout(timeoutId);\n if (error instanceof GeoError) throw error;\n if ((error as Error).name === 'AbortError') {\n throw new GeoError('Request timeout', 'TIMEOUT', 'openstreetmap');\n }\n throw new GeoError(\n `Failed to find POIs: ${(error as Error).message}`,\n 'POI_SEARCH_FAILED',\n 'openstreetmap',\n );\n }\n }\n\n /**\n * Build an Overpass QL query that collects nodes + ways with POI-shaped\n * tags within `radiusMeters` of the center point. `out center tags`\n * coerces ways/relations into point geometries so downstream mapping\n * doesn't have to deal with geometry types.\n */\n private buildOverpassQuery(\n latitude: number,\n longitude: number,\n radiusMeters: number,\n options: PoiSearchOptions,\n ): string {\n const around = `around:${radiusMeters},${latitude},${longitude}`;\n // `keyword` is documented as a free-text substring filter, but Overpass\n // interprets the right-hand side of `name~\"...\"` as a POSIX ERE. Escape\n // regex metacharacters so inputs like `C++`, `A.*`, or `Joes [Bar]`\n // match literally instead of silently changing the regex semantics or\n // producing an invalid query.\n const keywordFilter = options.keyword\n ? `[name~\"${escapeOverpassRegex(options.keyword)}\",i]`\n : '';\n const clauses: string[] = [];\n\n if (options.types && options.types.length > 0) {\n // Types filter: try each value against each POI tag key. We don't know\n // which key the caller intends (e.g. 'cafe' is an amenity; 'bakery'\n // could be either amenity or shop) so we fan out conservatively.\n for (const value of options.types) {\n const safe = value.replace(/\"/g, '\\\\\"');\n for (const key of POI_TAG_KEYS) {\n clauses.push(\n ` node(${around})[\"${key}\"=\"${safe}\"]${keywordFilter};`,\n );\n clauses.push(` way(${around})[\"${key}\"=\"${safe}\"]${keywordFilter};`);\n }\n }\n } else {\n for (const key of POI_TAG_KEYS) {\n clauses.push(` node(${around})[\"${key}\"]${keywordFilter};`);\n clauses.push(` way(${around})[\"${key}\"]${keywordFilter};`);\n }\n }\n\n return `[out:json][timeout:25];\\n(\\n${clauses.join('\\n')}\\n);\\nout center tags;`;\n }\n\n /**\n * Map an Overpass element to a standardized Location. Returns null for\n * tagless elements or ways without a `center` (which can happen when the\n * server falls back to geometry-less output under load).\n */\n private mapOverpassElementToLocation(\n element: OverpassElement,\n ): Location | null {\n const lat = element.lat ?? element.center?.lat;\n const lon = element.lon ?? element.center?.lon;\n if (lat == null || lon == null) return null;\n\n const tags = element.tags ?? {};\n const name =\n tags.name ||\n tags['name:en'] ||\n tags.brand ||\n tags.operator ||\n this.derivePoiLabelFromTags(tags) ||\n 'Unnamed place';\n\n return {\n id: `osm-${element.type}-${element.id}`,\n type: 'point_of_interest',\n name,\n latitude: lat,\n longitude: lon,\n addressComponents: {\n streetNumber: tags['addr:housenumber'],\n streetName: tags['addr:street'],\n city: tags['addr:city'],\n region: tags['addr:state'] || tags['addr:province'],\n country: tags['addr:country'],\n postalCode: tags['addr:postcode'],\n },\n countryCode: normalizeCountryCode(tags['addr:country']),\n raw: element,\n };\n }\n\n /**\n * Produce a readable label from a tag-only element (e.g. a shop with no\n * `name`). Picks the first POI-shaped tag value as a fallback so the\n * caller still gets something meaningful to show operators.\n */\n private derivePoiLabelFromTags(tags: Record<string, string>): string | null {\n for (const key of POI_TAG_KEYS) {\n const value = tags[key];\n if (value) return value.replace(/_/g, ' ');\n }\n return null;\n }\n\n /**\n * Map Nominatim result to standardized Location\n */\n private mapNominatimResultToLocation(result: NominatimResult): Location {\n const address = result.address || {};\n\n // Build address components\n const addressComponents: Location['addressComponents'] = {};\n\n if (address.house_number) {\n addressComponents.streetNumber = address.house_number;\n }\n if (address.road) {\n addressComponents.streetName = address.road;\n }\n if (address.city || address.town || address.village) {\n addressComponents.city = address.city || address.town || address.village;\n }\n if (address.state) {\n addressComponents.region = address.state;\n }\n if (address.country) {\n addressComponents.country = address.country;\n }\n if (address.postcode) {\n addressComponents.postalCode = address.postcode;\n }\n\n // Determine location type\n const type = mapOSMPlaceType(result.type || '', result.addresstype);\n\n // Parse coordinates\n const latitude = parseFloat(result.lat);\n const longitude = parseFloat(result.lon);\n\n return {\n id: `osm-${result.place_id}`,\n type,\n name: result.display_name,\n latitude,\n longitude,\n addressComponents,\n countryCode: normalizeCountryCode(address.country_code),\n raw: result,\n };\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import { GeoError } from "../index.js";
2
+ //#region src/shared/timezone-mode.ts
3
+ var MODES = [
4
+ "offline",
5
+ "api",
6
+ "none"
7
+ ];
8
+ var warnedOsmApiMode = false;
9
+ function resolveTimezoneLookup(value, provider) {
10
+ if (value === void 0 || value === null || value === "") return "offline";
11
+ const mode = typeof value === "string" ? value.trim().toLowerCase() : void 0;
12
+ if (!mode || !MODES.includes(mode)) throw new GeoError(`Invalid timezoneLookup ${JSON.stringify(value)} (option or HAVE_GEO_TIMEZONE_LOOKUP): expected 'offline', 'api', or 'none'.`, "INVALID_OPTION", provider);
13
+ if (mode === "api" && provider === "openstreetmap") {
14
+ if (!warnedOsmApiMode) {
15
+ warnedOsmApiMode = true;
16
+ console.warn("@happyvertical/geo: timezoneLookup: 'api' is only supported by the Google provider; OpenStreetMap uses the offline time zone table.");
17
+ }
18
+ return "offline";
19
+ }
20
+ return mode;
21
+ }
22
+ //#endregion
23
+ export { resolveTimezoneLookup as t };
24
+
25
+ //# sourceMappingURL=timezone-mode-DFovZtFK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timezone-mode-DFovZtFK.js","names":[],"sources":["../../src/shared/timezone-mode.ts"],"sourcesContent":["/**\n * Validation for the `timezoneLookup` adapter option.\n *\n * @internal\n */\n\nimport type { TimezoneLookupMode } from './timezone';\nimport { GeoError } from './types';\n\nconst MODES: readonly TimezoneLookupMode[] = ['offline', 'api', 'none'];\n\nlet warnedOsmApiMode = false;\n\n/**\n * Returns a valid `timezoneLookup` mode for a provider, defaulting to\n * `'offline'`. Values are trimmed and case-insensitive (environment variables\n * arrive as strings). Anything else throws a `GeoError` naming the accepted\n * values, rather than silently disabling or changing the lookup.\n *\n * `'api'` exists only for Google; OpenStreetMap has no time zone API, so it\n * warns once per process and uses `'offline'`.\n */\nexport function resolveTimezoneLookup(\n value: unknown,\n provider: 'google',\n): TimezoneLookupMode;\nexport function resolveTimezoneLookup(\n value: unknown,\n provider: 'openstreetmap',\n): 'offline' | 'none';\nexport function resolveTimezoneLookup(\n value: unknown,\n provider: 'google' | 'openstreetmap',\n): TimezoneLookupMode {\n if (value === undefined || value === null || value === '') {\n return 'offline';\n }\n\n const mode =\n typeof value === 'string'\n ? (value.trim().toLowerCase() as TimezoneLookupMode)\n : undefined;\n if (!mode || !MODES.includes(mode)) {\n throw new GeoError(\n `Invalid timezoneLookup ${JSON.stringify(value)} (option or HAVE_GEO_TIMEZONE_LOOKUP): expected 'offline', 'api', or 'none'.`,\n 'INVALID_OPTION',\n provider,\n );\n }\n\n if (mode === 'api' && provider === 'openstreetmap') {\n if (!warnedOsmApiMode) {\n warnedOsmApiMode = true;\n console.warn(\n \"@happyvertical/geo: timezoneLookup: 'api' is only supported by the Google provider; OpenStreetMap uses the offline time zone table.\",\n );\n }\n return 'offline';\n }\n\n return mode;\n}\n"],"mappings":";;AASA,IAAM,QAAuC;CAAC;CAAW;CAAO;AAAM;AAEtE,IAAI,mBAAmB;AAmBvB,SAAgB,sBACd,OACA,UACoB;CACpB,IAAI,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU,IACrD,OAAO;CAGT,MAAM,OACJ,OAAO,UAAU,WACZ,MAAM,KAAK,CAAC,CAAC,YAAY,IAC1B,KAAA;CACN,IAAI,CAAC,QAAQ,CAAC,MAAM,SAAS,IAAI,GAC/B,MAAM,IAAI,SACR,0BAA0B,KAAK,UAAU,KAAK,EAAE,+EAChD,kBACA,QACF;CAGF,IAAI,SAAS,SAAS,aAAa,iBAAiB;EAClD,IAAI,CAAC,kBAAkB;GACrB,mBAAmB;GACnB,QAAQ,KACN,qIACF;EACF;EACA,OAAO;CACT;CAEA,OAAO;AACT"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { GeoAdapter, GeoAdapterOptions } from './shared/types';
2
+ export * from './shared/timezone';
2
3
  export * from './shared/types';
3
4
  export * from './shared/utils';
4
5
  export { fetchStaticMap, type GoogleMapType, getOGMapUrl, getStaticMapUrl, type MapboxStyle, type StaticMapMarker, type StaticMapOptions, type StaticMapProvider, type StaticMapResult, } from './static-maps';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EAGlB,MAAM,gBAAgB,CAAC;AAGxB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAG/B,OAAO,EACL,cAAc,EACd,KAAK,aAAa,EAClB,WAAW,EACX,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,eAAe,GACrB,MAAM,eAAe,CAAC;AAoBvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,aAAa,CACjC,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GACvC,OAAO,CAAC,UAAU,CAAC,CA8CrB;AAED,gBAAgB;AAChB,eAAO,MAAM,2BAA2B,OAAO,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EAGlB,MAAM,gBAAgB,CAAC;AAGxB,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAG/B,OAAO,EACL,cAAc,EACd,KAAK,aAAa,EAClB,WAAW,EACX,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,eAAe,GACrB,MAAM,eAAe,CAAC;AAoBvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,aAAa,CACjC,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GACvC,OAAO,CAAC,UAAU,CAAC,CA+CrB;AAED,gBAAgB;AAChB,eAAO,MAAM,2BAA2B,OAAO,CAAC"}
package/dist/index.js CHANGED
@@ -1,77 +1,5 @@
1
1
  import { loadEnvConfig } from "@happyvertical/utils";
2
- //#region src/shared/types.ts
3
- /**
4
- * Base error class for geo operations.
5
- * All geo-specific errors extend this class and include a machine-readable `code`
6
- * and the `provider` name that produced the error.
7
- */
8
- var GeoError = class extends Error {
9
- code;
10
- provider;
11
- /**
12
- * @param message - Human-readable error description
13
- * @param code - Machine-readable error code (e.g. 'RATE_LIMIT', 'AUTH_ERROR')
14
- * @param provider - Provider that raised the error ('google' | 'openstreetmap')
15
- */
16
- constructor(message, code, provider) {
17
- super(message);
18
- this.code = code;
19
- this.provider = provider;
20
- this.name = "GeoError";
21
- }
22
- };
23
- /**
24
- * Thrown when the provider's rate limit has been exceeded.
25
- */
26
- var RateLimitError = class extends GeoError {
27
- /**
28
- * @param provider - Provider that raised the error
29
- * @param retryAfter - Seconds to wait before retrying (if known)
30
- */
31
- constructor(provider, retryAfter) {
32
- super(`Rate limit exceeded${retryAfter ? `, retry after ${retryAfter}s` : ""}`, "RATE_LIMIT", provider);
33
- this.name = "RateLimitError";
34
- }
35
- };
36
- /**
37
- * Thrown when the geocoding query is empty or malformed.
38
- */
39
- var InvalidQueryError = class extends GeoError {
40
- /**
41
- * @param query - The invalid query string
42
- * @param provider - Provider that raised the error
43
- */
44
- constructor(query, provider) {
45
- super(`Invalid query: ${query}`, "INVALID_QUERY", provider);
46
- this.name = "InvalidQueryError";
47
- }
48
- };
49
- /**
50
- * Thrown when the provider rejects the API key or credentials.
51
- */
52
- var AuthenticationError = class extends GeoError {
53
- /**
54
- * @param provider - Provider that raised the error
55
- */
56
- constructor(provider) {
57
- super("Authentication failed", "AUTH_ERROR", provider);
58
- this.name = "AuthenticationError";
59
- }
60
- };
61
- /**
62
- * Thrown when a geocoding query returns zero results.
63
- */
64
- var NoResultsError = class extends GeoError {
65
- /**
66
- * @param query - The query that produced no results
67
- * @param provider - Provider that raised the error
68
- */
69
- constructor(query, provider) {
70
- super(`No results found for query: ${query}`, "NO_RESULTS", provider);
71
- this.name = "NoResultsError";
72
- }
73
- };
74
- //#endregion
2
+ import tzLookup from "@photostructure/tz-lookup";
75
3
  //#region src/shared/utils.ts
76
4
  /**
77
5
  * Map Google Maps place types to standardized location types.
@@ -157,6 +85,119 @@ function validateCoordinates(latitude, longitude) {
157
85
  return { valid: true };
158
86
  }
159
87
  //#endregion
88
+ //#region src/shared/timezone.ts
89
+ /**
90
+ * IANA time zone resolution for geocoded coordinates.
91
+ *
92
+ * Offline lookups use `@photostructure/tz-lookup` (CC0-1.0, no runtime
93
+ * dependencies, ~90 KB), a maintained fork of `tz-lookup` whose compressed
94
+ * boundary table is regenerated from timezone-boundary-builder releases. It
95
+ * answers in microseconds without network access, so every provider can fill
96
+ * `Location.timezone` without extra billed requests or rate-limit pressure.
97
+ *
98
+ * The table trades precision for size: within a few kilometres of a zone
99
+ * boundary it can pick the neighbouring zone. Callers that need exact answers
100
+ * near borders can use the Google provider's `timezoneLookup: 'api'` mode.
101
+ */
102
+ /**
103
+ * Returns the IANA time zone for a coordinate using the bundled offline
104
+ * boundary table, or `undefined` when the coordinate is invalid.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * timezoneForCoordinates(53.5461, -113.4938); // 'America/Edmonton'
109
+ * ```
110
+ */
111
+ function timezoneForCoordinates(latitude, longitude) {
112
+ if (!validateCoordinates(latitude, longitude).valid) return;
113
+ return tzLookup(latitude, longitude);
114
+ }
115
+ /**
116
+ * Fills `timezone` on each location from the offline table, keeping any value
117
+ * a provider already set. Returns the same array for chaining.
118
+ */
119
+ function withOfflineTimezones(locations) {
120
+ for (const location of locations) {
121
+ if (location.timezone) continue;
122
+ const timezone = timezoneForCoordinates(location.latitude, location.longitude);
123
+ if (timezone) location.timezone = timezone;
124
+ }
125
+ return locations;
126
+ }
127
+ //#endregion
128
+ //#region src/shared/types.ts
129
+ /**
130
+ * Base error class for geo operations.
131
+ * All geo-specific errors extend this class and include a machine-readable `code`
132
+ * and the `provider` name that produced the error.
133
+ */
134
+ var GeoError = class extends Error {
135
+ code;
136
+ provider;
137
+ /**
138
+ * @param message - Human-readable error description
139
+ * @param code - Machine-readable error code (e.g. 'RATE_LIMIT', 'AUTH_ERROR')
140
+ * @param provider - Provider that raised the error ('google' | 'openstreetmap')
141
+ */
142
+ constructor(message, code, provider) {
143
+ super(message);
144
+ this.code = code;
145
+ this.provider = provider;
146
+ this.name = "GeoError";
147
+ }
148
+ };
149
+ /**
150
+ * Thrown when the provider's rate limit has been exceeded.
151
+ */
152
+ var RateLimitError = class extends GeoError {
153
+ /**
154
+ * @param provider - Provider that raised the error
155
+ * @param retryAfter - Seconds to wait before retrying (if known)
156
+ */
157
+ constructor(provider, retryAfter) {
158
+ super(`Rate limit exceeded${retryAfter ? `, retry after ${retryAfter}s` : ""}`, "RATE_LIMIT", provider);
159
+ this.name = "RateLimitError";
160
+ }
161
+ };
162
+ /**
163
+ * Thrown when the geocoding query is empty or malformed.
164
+ */
165
+ var InvalidQueryError = class extends GeoError {
166
+ /**
167
+ * @param query - The invalid query string
168
+ * @param provider - Provider that raised the error
169
+ */
170
+ constructor(query, provider) {
171
+ super(`Invalid query: ${query}`, "INVALID_QUERY", provider);
172
+ this.name = "InvalidQueryError";
173
+ }
174
+ };
175
+ /**
176
+ * Thrown when the provider rejects the API key or credentials.
177
+ */
178
+ var AuthenticationError = class extends GeoError {
179
+ /**
180
+ * @param provider - Provider that raised the error
181
+ */
182
+ constructor(provider) {
183
+ super("Authentication failed", "AUTH_ERROR", provider);
184
+ this.name = "AuthenticationError";
185
+ }
186
+ };
187
+ /**
188
+ * Thrown when a geocoding query returns zero results.
189
+ */
190
+ var NoResultsError = class extends GeoError {
191
+ /**
192
+ * @param query - The query that produced no results
193
+ * @param provider - Provider that raised the error
194
+ */
195
+ constructor(query, provider) {
196
+ super(`No results found for query: ${query}`, "NO_RESULTS", provider);
197
+ this.name = "NoResultsError";
198
+ }
199
+ };
200
+ //#endregion
160
201
  //#region src/static-maps.ts
161
202
  /**
162
203
  * Generate Mapbox static map URL
@@ -379,7 +420,8 @@ async function getGeoAdapter(options = {}) {
379
420
  timeout: "number",
380
421
  maxResults: "number",
381
422
  rateLimitDelay: "number",
382
- userAgent: "string"
423
+ userAgent: "string",
424
+ timezoneLookup: "string"
383
425
  }
384
426
  });
385
427
  if (config.provider === "google" && !config.apiKey) {
@@ -389,11 +431,11 @@ async function getGeoAdapter(options = {}) {
389
431
  if (!config.provider) throw new Error("Provider is required. Set via options.provider or HAVE_GEO_PROVIDER environment variable.");
390
432
  const fullOptions = config;
391
433
  if (isGoogleMapsOptions(fullOptions)) {
392
- const { GoogleMapsProvider } = await import("./chunks/google-CZUa1ho0.js");
434
+ const { GoogleMapsProvider } = await import("./chunks/google-Cb-ytnTf.js");
393
435
  return new GoogleMapsProvider(fullOptions);
394
436
  }
395
437
  if (isOpenStreetMapOptions(fullOptions)) {
396
- const { OpenStreetMapProvider } = await import("./chunks/openstreetmap-St4Kw-vS.js");
438
+ const { OpenStreetMapProvider } = await import("./chunks/openstreetmap-D55wPXMO.js");
397
439
  return new OpenStreetMapProvider(fullOptions);
398
440
  }
399
441
  throw new Error(`Unsupported provider: ${fullOptions.provider}`);
@@ -401,6 +443,6 @@ async function getGeoAdapter(options = {}) {
401
443
  /** @internal */
402
444
  var PACKAGE_VERSION_INITIALIZED = true;
403
445
  //#endregion
404
- export { AuthenticationError, GeoError, InvalidQueryError, NoResultsError, PACKAGE_VERSION_INITIALIZED, RateLimitError, fetchStaticMap, getGeoAdapter, getOGMapUrl, getStaticMapUrl, isValidLatitude, isValidLongitude, mapGooglePlaceType, mapOSMPlaceType, normalizeCountryCode, validateCoordinates };
446
+ export { AuthenticationError, GeoError, InvalidQueryError, NoResultsError, PACKAGE_VERSION_INITIALIZED, RateLimitError, fetchStaticMap, getGeoAdapter, getOGMapUrl, getStaticMapUrl, isValidLatitude, isValidLongitude, mapGooglePlaceType, mapOSMPlaceType, normalizeCountryCode, timezoneForCoordinates, validateCoordinates, withOfflineTimezones };
405
447
 
406
448
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/shared/types.ts","../src/shared/utils.ts","../src/static-maps.ts","../src/index.ts"],"sourcesContent":["/**\n * Core types and interfaces for the Geo library\n */\n\n/**\n * Standardized location data structure\n */\nexport interface Location {\n /**\n * Unique identifier for the location (from provider)\n */\n id: string;\n\n /**\n * Type of location\n */\n type:\n | 'country'\n | 'region'\n | 'city'\n | 'address'\n | 'point_of_interest'\n | 'unknown';\n\n /**\n * Full formatted name/address of the location\n */\n name: string;\n\n /**\n * Latitude coordinate\n */\n latitude: number;\n\n /**\n * Longitude coordinate\n */\n longitude: number;\n\n /**\n * Address components (all optional)\n */\n addressComponents: {\n streetNumber?: string;\n streetName?: string;\n city?: string;\n region?: string;\n country?: string;\n postalCode?: string;\n };\n\n /**\n * ISO 3166-1 alpha-2 country code\n */\n countryCode: string;\n\n /**\n * Timezone identifier (optional, populated when provider returns it)\n */\n timezone?: string;\n\n /**\n * Raw response from the provider (for debugging or provider-specific data)\n */\n raw: any;\n}\n\n/**\n * Options for POI (point-of-interest) searches.\n *\n * `types` and `keyword` are both forwarded to the backing provider but each\n * provider interprets them slightly differently:\n *\n * - **Google**: `types[0]` becomes the request's `type` filter (Places API\n * accepts a single type per request; additional entries are ignored).\n * `keyword` is a free-text match across name/type/address/reviews.\n * - **OpenStreetMap (Overpass)**: `types` are matched against\n * `amenity`, `shop`, and `tourism` tag values (e.g. `'cafe'`,\n * `'supermarket'`, `'museum'`). When omitted, the provider searches across\n * a broad set of POI-ish tag keys (`amenity`, `shop`, `tourism`,\n * `leisure`, `office`, `historic`). `keyword` is appended as a substring\n * filter on the `name` tag.\n */\nexport interface PoiSearchOptions {\n /** Filter results to POIs matching these category values. See notes above. */\n types?: string[];\n /** Free-text keyword to narrow the search. */\n keyword?: string;\n /** Max results to return. Default 20. */\n limit?: number;\n /** Preferred language for place names (Google only). */\n language?: string;\n}\n\n/**\n * Geo provider interface - all providers must implement lookup and\n * reverseGeocode. `findPoisNear` is optional — providers implement it when\n * they support POI discovery beyond reverse geocoding. Callers that need to\n * know whether a given instance supports POI search should feature-detect:\n *\n * ```ts\n * if (typeof adapter.findPoisNear === 'function') { ... }\n * ```\n */\nexport interface GeoProvider {\n /**\n * Look up locations based on a query string\n * @param query - Search string (address, city, country, POI, etc.)\n * @returns Promise resolving to array of matching Location objects\n */\n lookup(query: string): Promise<Location[]>;\n\n /**\n * Reverse geocode from coordinates to location\n * @param latitude - Latitude coordinate\n * @param longitude - Longitude coordinate\n * @returns Promise resolving to array of matching Location objects\n */\n reverseGeocode(latitude: number, longitude: number): Promise<Location[]>;\n\n /**\n * Find POIs (point-of-interest places — businesses, landmarks, amenities)\n * within a radius of a coordinate. Returns locations with\n * `type: 'point_of_interest'` whose coords lie inside the requested\n * radius, sorted by the provider's relevance ranking.\n *\n * @param latitude - Center latitude\n * @param longitude - Center longitude\n * @param radiusMeters - Search radius in meters\n * @param options - Optional filters\n * @returns Promise resolving to array of matching Location objects\n */\n findPoisNear?(\n latitude: number,\n longitude: number,\n radiusMeters: number,\n options?: PoiSearchOptions,\n ): Promise<Location[]>;\n}\n\n/**\n * Geo adapter interface (structurally identical to GeoProvider)\n */\nexport interface GeoAdapter {\n /**\n * Look up locations based on a query string\n * @param query - Search string (address, city, country, POI, etc.)\n * @returns Promise resolving to array of matching Location objects\n */\n lookup(query: string): Promise<Location[]>;\n\n /**\n * Reverse geocode from coordinates to location\n * @param latitude - Latitude coordinate\n * @param longitude - Longitude coordinate\n * @returns Promise resolving to array of matching Location objects\n */\n reverseGeocode(latitude: number, longitude: number): Promise<Location[]>;\n\n /**\n * Find POIs near a coordinate. Optional — see `GeoProvider.findPoisNear`.\n */\n findPoisNear?(\n latitude: number,\n longitude: number,\n radiusMeters: number,\n options?: PoiSearchOptions,\n ): Promise<Location[]>;\n}\n\n/**\n * Base configuration options for all providers\n */\nexport interface BaseGeoOptions {\n /**\n * Request timeout in milliseconds\n */\n timeout?: number;\n\n /**\n * Maximum number of results to return\n */\n maxResults?: number;\n}\n\n/**\n * Google Maps provider options\n */\nexport interface GoogleMapsOptions extends BaseGeoOptions {\n provider: 'google';\n /**\n * Google Maps API key\n */\n apiKey: string;\n}\n\n/**\n * OpenStreetMap provider options\n */\nexport interface OpenStreetMapOptions extends BaseGeoOptions {\n provider: 'openstreetmap';\n /**\n * Custom User-Agent for OSM requests (optional, defaults to package name)\n */\n userAgent?: string;\n /**\n * Rate limit delay in milliseconds (default: 1000ms for 1 req/sec)\n */\n rateLimitDelay?: number;\n}\n\n/**\n * Union type for all provider options\n */\nexport type GeoAdapterOptions = GoogleMapsOptions | OpenStreetMapOptions;\n\n/**\n * Base error class for geo operations.\n * All geo-specific errors extend this class and include a machine-readable `code`\n * and the `provider` name that produced the error.\n */\nexport class GeoError extends Error {\n /**\n * @param message - Human-readable error description\n * @param code - Machine-readable error code (e.g. 'RATE_LIMIT', 'AUTH_ERROR')\n * @param provider - Provider that raised the error ('google' | 'openstreetmap')\n */\n constructor(\n message: string,\n public code: string,\n public provider?: string,\n ) {\n super(message);\n this.name = 'GeoError';\n }\n}\n\n/**\n * Thrown when the provider's rate limit has been exceeded.\n */\nexport class RateLimitError extends GeoError {\n /**\n * @param provider - Provider that raised the error\n * @param retryAfter - Seconds to wait before retrying (if known)\n */\n constructor(provider?: string, retryAfter?: number) {\n super(\n `Rate limit exceeded${retryAfter ? `, retry after ${retryAfter}s` : ''}`,\n 'RATE_LIMIT',\n provider,\n );\n this.name = 'RateLimitError';\n }\n}\n\n/**\n * Thrown when the geocoding query is empty or malformed.\n */\nexport class InvalidQueryError extends GeoError {\n /**\n * @param query - The invalid query string\n * @param provider - Provider that raised the error\n */\n constructor(query: string, provider?: string) {\n super(`Invalid query: ${query}`, 'INVALID_QUERY', provider);\n this.name = 'InvalidQueryError';\n }\n}\n\n/**\n * Thrown when the provider rejects the API key or credentials.\n */\nexport class AuthenticationError extends GeoError {\n /**\n * @param provider - Provider that raised the error\n */\n constructor(provider?: string) {\n super('Authentication failed', 'AUTH_ERROR', provider);\n this.name = 'AuthenticationError';\n }\n}\n\n/**\n * Thrown when a geocoding query returns zero results.\n */\nexport class NoResultsError extends GeoError {\n /**\n * @param query - The query that produced no results\n * @param provider - Provider that raised the error\n */\n constructor(query: string, provider?: string) {\n super(`No results found for query: ${query}`, 'NO_RESULTS', provider);\n this.name = 'NoResultsError';\n }\n}\n","/**\n * Utility functions for geo operations\n */\n\nimport type { Location } from './types';\n\n/**\n * Map Google Maps place types to standardized location types.\n * Checks types in priority order: address, city, region, country, point_of_interest.\n *\n * @param types - Array of Google Maps place type strings (e.g. 'street_address', 'locality')\n * @returns The standardized location type\n */\nexport function mapGooglePlaceType(types: string[]): Location['type'] {\n if (!types || types.length === 0) return 'unknown';\n\n // Check in priority order\n if (types.includes('street_address') || types.includes('premise')) {\n return 'address';\n }\n if (types.includes('locality') || types.includes('postal_town')) {\n return 'city';\n }\n if (\n types.includes('administrative_area_level_1') ||\n types.includes('administrative_area_level_2')\n ) {\n return 'region';\n }\n if (types.includes('country')) {\n return 'country';\n }\n if (types.includes('point_of_interest') || types.includes('establishment')) {\n return 'point_of_interest';\n }\n\n return 'unknown';\n}\n\n/**\n * Map OpenStreetMap place types to standardized location types.\n * Uses both the Nominatim `type` and `addresstype` fields for classification.\n *\n * @param type - Nominatim result type (e.g. 'house', 'city', 'state')\n * @param addressType - Nominatim addresstype field (optional secondary classifier)\n * @returns The standardized location type\n */\nexport function mapOSMPlaceType(\n type: string,\n addressType?: string,\n): Location['type'] {\n // OSM uses different classification - check both type and addresstype\n const checkType = (type || '').toLowerCase();\n const checkAddressType = (addressType || '').toLowerCase();\n\n if (\n checkType === 'house' ||\n checkType === 'building' ||\n checkAddressType === 'house'\n ) {\n return 'address';\n }\n\n if (\n checkType === 'city' ||\n checkType === 'town' ||\n checkType === 'village' ||\n checkType === 'hamlet' ||\n checkAddressType === 'city' ||\n checkAddressType === 'town' ||\n checkAddressType === 'village'\n ) {\n return 'city';\n }\n\n if (\n checkType === 'state' ||\n checkType === 'province' ||\n checkType === 'region' ||\n checkAddressType === 'state'\n ) {\n return 'region';\n }\n\n if (checkType === 'country' || checkAddressType === 'country') {\n return 'country';\n }\n\n if (\n checkType === 'attraction' ||\n checkType === 'tourism' ||\n checkType === 'amenity'\n ) {\n return 'point_of_interest';\n }\n\n return 'unknown';\n}\n\n/**\n * Normalize a country code to uppercase ISO 3166-1 format.\n * Returns 'XX' for undefined, empty, or unrecognized inputs.\n *\n * @param code - Country code string (alpha-2 or alpha-3)\n * @returns Uppercase country code, or 'XX' if unknown\n */\nexport function normalizeCountryCode(code: string | undefined): string {\n if (!code) return 'XX'; // Unknown country code\n\n // Already uppercase 2-letter code\n const normalized = code.toUpperCase().trim();\n\n // If it's already 2 letters, return it\n if (normalized.length === 2) {\n return normalized;\n }\n\n // If it's 3 letters (alpha-3), we would need a mapping table\n // For now, return as-is or 'XX' for unknown\n return normalized.length === 3 ? normalized : 'XX';\n}\n\n/**\n * Validate that a latitude value is within the valid range (-90 to 90).\n *\n * @param lat - Latitude value to validate\n * @returns `true` if the value is a number between -90 and 90 inclusive\n */\nexport function isValidLatitude(lat: number): boolean {\n return typeof lat === 'number' && lat >= -90 && lat <= 90;\n}\n\n/**\n * Validate that a longitude value is within the valid range (-180 to 180).\n *\n * @param lng - Longitude value to validate\n * @returns `true` if the value is a number between -180 and 180 inclusive\n */\nexport function isValidLongitude(lng: number): boolean {\n return typeof lng === 'number' && lng >= -180 && lng <= 180;\n}\n\n/**\n * Validate that both latitude and longitude are within their valid ranges.\n *\n * @param latitude - Latitude value (-90 to 90)\n * @param longitude - Longitude value (-180 to 180)\n * @returns Object with `valid: true` if both are valid, or `valid: false` with an `error` message\n */\nexport function validateCoordinates(\n latitude: number,\n longitude: number,\n): { valid: boolean; error?: string } {\n if (!isValidLatitude(latitude)) {\n return {\n valid: false,\n error: `Invalid latitude: ${latitude}. Must be between -90 and 90.`,\n };\n }\n\n if (!isValidLongitude(longitude)) {\n return {\n valid: false,\n error: `Invalid longitude: ${longitude}. Must be between -180 and 180.`,\n };\n }\n\n return { valid: true };\n}\n","/**\n * Static Map Generation\n *\n * Generates static map URLs and fetches map images for location-based content.\n * Supports Mapbox and Google Maps providers.\n *\n * @example\n * ```typescript\n * import { getStaticMapUrl, fetchStaticMap } from '@happyvertical/geo';\n *\n * // Get URL for embedding\n * const url = getStaticMapUrl(53.5461, -113.4938, {\n * provider: 'mapbox',\n * zoom: 14,\n * width: 1200,\n * height: 630\n * });\n *\n * // Fetch the actual image\n * const buffer = await fetchStaticMap(53.5461, -113.4938, {\n * provider: 'mapbox'\n * });\n * await writeFile('map.png', buffer);\n * ```\n */\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Static map provider\n */\nexport type StaticMapProvider = 'mapbox' | 'google';\n\n/**\n * Mapbox map styles\n * @see https://docs.mapbox.com/api/maps/styles/\n */\nexport type MapboxStyle =\n | 'streets-v12'\n | 'outdoors-v12'\n | 'light-v11'\n | 'dark-v11'\n | 'satellite-v9'\n | 'satellite-streets-v12';\n\n/**\n * Google Maps map types\n * @see https://developers.google.com/maps/documentation/maps-static/start\n */\nexport type GoogleMapType = 'roadmap' | 'satellite' | 'terrain' | 'hybrid';\n\n/**\n * Marker definition for the map\n */\nexport interface StaticMapMarker {\n /** Latitude */\n latitude: number;\n /** Longitude */\n longitude: number;\n /** Marker color (hex or named color) */\n color?: string;\n /** Marker label (single character for Google, any for Mapbox) */\n label?: string;\n /** Marker size ('small' | 'medium' | 'large') */\n size?: 'small' | 'medium' | 'large';\n}\n\n/**\n * Options for static map generation\n */\nexport interface StaticMapOptions {\n /**\n * Map provider\n * @default 'mapbox'\n */\n provider?: StaticMapProvider;\n\n /**\n * Image width in pixels\n * @default 1200\n */\n width?: number;\n\n /**\n * Image height in pixels\n * @default 630\n */\n height?: number;\n\n /**\n * Zoom level (1-20)\n * @default 14\n */\n zoom?: number;\n\n /**\n * Marker color (hex without # or named color)\n * @default 'e74c3c' (red)\n */\n markerColor?: string;\n\n /**\n * Show a marker at the center point\n * @default true\n */\n showMarker?: boolean;\n\n /**\n * Mapbox access token (required for Mapbox provider)\n * Falls back to MAPBOX_ACCESS_TOKEN env var\n */\n mapboxToken?: string;\n\n /**\n * Google Maps API key (required for Google provider)\n * Falls back to GOOGLE_MAPS_API_KEY env var\n */\n googleApiKey?: string;\n\n /**\n * Mapbox style\n * @default 'streets-v12'\n */\n mapboxStyle?: MapboxStyle;\n\n /**\n * Google map type\n * @default 'roadmap'\n */\n googleMapType?: GoogleMapType;\n\n /**\n * Pixel density (1 or 2 for retina)\n * @default 1\n */\n scale?: 1 | 2;\n\n /**\n * Additional markers to display\n */\n markers?: StaticMapMarker[];\n}\n\n/**\n * Result from fetching a static map\n */\nexport interface StaticMapResult {\n /** PNG image buffer */\n buffer: Buffer;\n /** Image width */\n width: number;\n /** Image height */\n height: number;\n /** MIME type */\n mimeType: 'image/png';\n /** The URL that was fetched */\n url: string;\n}\n\n// ============================================================================\n// URL Generators\n// ============================================================================\n\n/**\n * Generate Mapbox static map URL\n */\nfunction getMapboxUrl(\n latitude: number,\n longitude: number,\n options: StaticMapOptions,\n): string {\n const token =\n options.mapboxToken ??\n process.env.MAPBOX_ACCESS_TOKEN ??\n process.env.MAPBOX_TOKEN;\n if (!token) {\n throw new Error(\n 'Mapbox access token required. Provide via options.mapboxToken or MAPBOX_ACCESS_TOKEN env var.',\n );\n }\n\n const width = options.width ?? 1200;\n const height = options.height ?? 630;\n const zoom = options.zoom ?? 14;\n const style = options.mapboxStyle ?? 'streets-v12';\n const scale = options.scale ?? 1;\n const showMarker = options.showMarker ?? true;\n\n // Build overlay for marker\n let overlay = '';\n if (showMarker) {\n const color = (options.markerColor ?? 'e74c3c').replace('#', '');\n // Mapbox marker format: pin-{size}-{label}+{color}({lon},{lat})\n overlay = `pin-l+${color}(${longitude},${latitude})/`;\n }\n\n // Add additional markers\n if (options.markers?.length) {\n const markerOverlays = options.markers.map((m) => {\n const color = (m.color ?? 'e74c3c').replace('#', '');\n const size = m.size === 'small' ? 's' : m.size === 'large' ? 'l' : 'm';\n const label = m.label ? `-${m.label}` : '';\n return `pin-${size}${label}+${color}(${m.longitude},${m.latitude})`;\n });\n overlay = markerOverlays.join(',') + '/';\n }\n\n const retinaStr = scale === 2 ? '@2x' : '';\n\n return `https://api.mapbox.com/styles/v1/mapbox/${style}/static/${overlay}${longitude},${latitude},${zoom}/${width}x${height}${retinaStr}?access_token=${token}`;\n}\n\n/**\n * Generate Google Maps static map URL\n */\nfunction getGoogleUrl(\n latitude: number,\n longitude: number,\n options: StaticMapOptions,\n): string {\n const apiKey =\n options.googleApiKey ??\n process.env.GOOGLE_MAPS_API_KEY ??\n process.env.GOOGLE_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'Google Maps API key required. Provide via options.googleApiKey or GOOGLE_MAPS_API_KEY env var.',\n );\n }\n\n const width = options.width ?? 1200;\n const height = options.height ?? 630;\n const zoom = options.zoom ?? 14;\n const mapType = options.googleMapType ?? 'roadmap';\n const scale = options.scale ?? 1;\n const showMarker = options.showMarker ?? true;\n\n const params = new URLSearchParams({\n center: `${latitude},${longitude}`,\n zoom: zoom.toString(),\n size: `${width}x${height}`,\n maptype: mapType,\n scale: scale.toString(),\n key: apiKey,\n });\n\n // Add center marker\n if (showMarker) {\n const color = (options.markerColor ?? 'red').replace('#', '0x');\n params.append('markers', `color:${color}|${latitude},${longitude}`);\n }\n\n // Add additional markers\n if (options.markers?.length) {\n for (const m of options.markers) {\n const color = (m.color ?? 'red').replace('#', '0x');\n const size = m.size ?? 'medium';\n const label = m.label ? `|label:${m.label.charAt(0).toUpperCase()}` : '';\n params.append(\n 'markers',\n `color:${color}|size:${size}${label}|${m.latitude},${m.longitude}`,\n );\n }\n }\n\n return `https://maps.googleapis.com/maps/api/staticmap?${params.toString()}`;\n}\n\n// ============================================================================\n// Main Functions\n// ============================================================================\n\n/**\n * Generate a static map URL\n *\n * Returns a URL that can be used directly in <img> tags or fetched for processing.\n *\n * @param latitude - Center latitude\n * @param longitude - Center longitude\n * @param options - Map options\n * @returns URL string for the static map\n *\n * @example Mapbox (default)\n * ```typescript\n * const url = getStaticMapUrl(53.5461, -113.4938, {\n * provider: 'mapbox',\n * zoom: 14,\n * mapboxStyle: 'streets-v12'\n * });\n * ```\n *\n * @example Google Maps\n * ```typescript\n * const url = getStaticMapUrl(53.5461, -113.4938, {\n * provider: 'google',\n * zoom: 14,\n * googleMapType: 'roadmap'\n * });\n * ```\n *\n * @example With custom markers\n * ```typescript\n * const url = getStaticMapUrl(53.5461, -113.4938, {\n * showMarker: false, // Hide center marker\n * markers: [\n * { latitude: 53.5461, longitude: -113.4938, color: 'blue', label: 'A' },\n * { latitude: 53.5500, longitude: -113.4900, color: 'green', label: 'B' }\n * ]\n * });\n * ```\n */\nexport function getStaticMapUrl(\n latitude: number,\n longitude: number,\n options: StaticMapOptions = {},\n): string {\n const provider = options.provider ?? 'mapbox';\n\n switch (provider) {\n case 'google':\n return getGoogleUrl(latitude, longitude, options);\n case 'mapbox':\n default:\n return getMapboxUrl(latitude, longitude, options);\n }\n}\n\n/**\n * Fetch a static map image\n *\n * Downloads the map image and returns it as a Buffer.\n *\n * @param latitude - Center latitude\n * @param longitude - Center longitude\n * @param options - Map options\n * @returns Promise resolving to the map image result\n *\n * @example\n * ```typescript\n * const result = await fetchStaticMap(53.5461, -113.4938, {\n * provider: 'mapbox',\n * width: 1200,\n * height: 630\n * });\n *\n * await fs.writeFile('map.png', result.buffer);\n * console.log(`Fetched ${result.width}x${result.height} map`);\n * ```\n */\nexport async function fetchStaticMap(\n latitude: number,\n longitude: number,\n options: StaticMapOptions = {},\n): Promise<StaticMapResult> {\n const width = options.width ?? 1200;\n const height = options.height ?? 630;\n const url = getStaticMapUrl(latitude, longitude, options);\n\n const response = await fetch(url);\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Failed to fetch static map: ${response.status} ${response.statusText} - ${errorText}`,\n );\n }\n\n const arrayBuffer = await response.arrayBuffer();\n const buffer = Buffer.from(arrayBuffer);\n\n return {\n buffer,\n width,\n height,\n mimeType: 'image/png',\n url,\n };\n}\n\n/**\n * Generate OG-sized static map URL (1200x630)\n *\n * Convenience function for generating Open Graph / social media sized maps.\n *\n * @param latitude - Center latitude\n * @param longitude - Center longitude\n * @param options - Map options (width/height ignored, set to 1200x630)\n * @returns URL string for the static map\n *\n * @example\n * ```typescript\n * const url = getOGMapUrl(53.5461, -113.4938);\n * // Returns 1200x630 map URL\n * ```\n */\nexport function getOGMapUrl(\n latitude: number,\n longitude: number,\n options: Omit<StaticMapOptions, 'width' | 'height'> = {},\n): string {\n return getStaticMapUrl(latitude, longitude, {\n ...options,\n width: 1200,\n height: 630,\n });\n}\n","/**\n * Geo package entry point\n * Provides standardized geographical information interface\n */\n\nimport { loadEnvConfig } from '@happyvertical/utils';\nimport type {\n GeoAdapter,\n GeoAdapterOptions,\n GoogleMapsOptions,\n OpenStreetMapOptions,\n} from './shared/types';\n\n// Export all types\nexport * from './shared/types';\nexport * from './shared/utils';\n\n// Export static map generation\nexport {\n fetchStaticMap,\n type GoogleMapType,\n getOGMapUrl,\n getStaticMapUrl,\n type MapboxStyle,\n type StaticMapMarker,\n type StaticMapOptions,\n type StaticMapProvider,\n type StaticMapResult,\n} from './static-maps';\n\n/**\n * Type guard for Google Maps options\n */\nfunction isGoogleMapsOptions(\n options: GeoAdapterOptions,\n): options is GoogleMapsOptions {\n return options.provider === 'google';\n}\n\n/**\n * Type guard for OpenStreetMap options\n */\nfunction isOpenStreetMapOptions(\n options: GeoAdapterOptions,\n): options is OpenStreetMapOptions {\n return options.provider === 'openstreetmap';\n}\n\n/**\n * Factory function to create a geo adapter instance\n *\n * Supports configuration via environment variables using the pattern:\n * - HAVE_GEO_PROVIDER → provider ('google' | 'openstreetmap')\n * - GOOGLE_MAPS_API_KEY → apiKey (for Google Maps provider)\n *\n * User-provided options always take precedence over environment variables.\n *\n * @param options - Configuration options for the geo provider (optional)\n * @returns Promise resolving to a geo adapter that implements GeoAdapter\n *\n * @example\n * ```typescript\n * // Create Google Maps adapter with explicit options\n * const googleGeo = await getGeoAdapter({\n * provider: 'google',\n * apiKey: process.env.GOOGLE_MAPS_API_KEY!\n * });\n *\n * // Create adapter using environment variables\n * // HAVE_GEO_PROVIDER=google\n * // GOOGLE_MAPS_API_KEY=your-api-key\n * const geoFromEnv = await getGeoAdapter();\n *\n * // Create OpenStreetMap adapter\n * const osmGeo = await getGeoAdapter({\n * provider: 'openstreetmap'\n * });\n *\n * // Use the adapter\n * const locations = await googleGeo.lookup('Eiffel Tower');\n * const coords = await osmGeo.reverseGeocode(48.8584, 2.2945);\n * ```\n */\nexport async function getGeoAdapter(\n options: Partial<GeoAdapterOptions> = {},\n): Promise<GeoAdapter> {\n // Load configuration from environment variables\n // Cast to any to handle union type with loadEnvConfig\n const config = loadEnvConfig(options as any, {\n packageName: 'geo',\n schema: {\n provider: 'string',\n timeout: 'number',\n maxResults: 'number',\n rateLimitDelay: 'number',\n userAgent: 'string',\n },\n }) as Partial<GeoAdapterOptions>;\n\n // Handle Google Maps API key from environment (not using HAVE_GEO_ prefix)\n if (config.provider === 'google' && !config.apiKey) {\n const apiKey = process.env.GOOGLE_MAPS_API_KEY;\n if (apiKey) {\n config.apiKey = apiKey;\n }\n }\n\n // Validate that we have a provider\n if (!config.provider) {\n throw new Error(\n 'Provider is required. Set via options.provider or HAVE_GEO_PROVIDER environment variable.',\n );\n }\n\n // Cast to full options type for provider-specific handling\n const fullOptions = config as GeoAdapterOptions;\n\n if (isGoogleMapsOptions(fullOptions)) {\n const { GoogleMapsProvider } = await import('./providers/google.js');\n return new GoogleMapsProvider(fullOptions);\n }\n\n if (isOpenStreetMapOptions(fullOptions)) {\n const { OpenStreetMapProvider } = await import(\n './providers/openstreetmap.js'\n );\n return new OpenStreetMapProvider(fullOptions);\n }\n\n // This should never happen due to TypeScript's discriminated union\n throw new Error(`Unsupported provider: ${(fullOptions as any).provider}`);\n}\n\n/** @internal */\nexport const PACKAGE_VERSION_INITIALIZED = true;\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/shared/utils.ts","../src/shared/timezone.ts","../src/shared/types.ts","../src/static-maps.ts","../src/index.ts"],"sourcesContent":["/**\n * Utility functions for geo operations\n */\n\nimport type { Location } from './types';\n\n/**\n * Map Google Maps place types to standardized location types.\n * Checks types in priority order: address, city, region, country, point_of_interest.\n *\n * @param types - Array of Google Maps place type strings (e.g. 'street_address', 'locality')\n * @returns The standardized location type\n */\nexport function mapGooglePlaceType(types: string[]): Location['type'] {\n if (!types || types.length === 0) return 'unknown';\n\n // Check in priority order\n if (types.includes('street_address') || types.includes('premise')) {\n return 'address';\n }\n if (types.includes('locality') || types.includes('postal_town')) {\n return 'city';\n }\n if (\n types.includes('administrative_area_level_1') ||\n types.includes('administrative_area_level_2')\n ) {\n return 'region';\n }\n if (types.includes('country')) {\n return 'country';\n }\n if (types.includes('point_of_interest') || types.includes('establishment')) {\n return 'point_of_interest';\n }\n\n return 'unknown';\n}\n\n/**\n * Map OpenStreetMap place types to standardized location types.\n * Uses both the Nominatim `type` and `addresstype` fields for classification.\n *\n * @param type - Nominatim result type (e.g. 'house', 'city', 'state')\n * @param addressType - Nominatim addresstype field (optional secondary classifier)\n * @returns The standardized location type\n */\nexport function mapOSMPlaceType(\n type: string,\n addressType?: string,\n): Location['type'] {\n // OSM uses different classification - check both type and addresstype\n const checkType = (type || '').toLowerCase();\n const checkAddressType = (addressType || '').toLowerCase();\n\n if (\n checkType === 'house' ||\n checkType === 'building' ||\n checkAddressType === 'house'\n ) {\n return 'address';\n }\n\n if (\n checkType === 'city' ||\n checkType === 'town' ||\n checkType === 'village' ||\n checkType === 'hamlet' ||\n checkAddressType === 'city' ||\n checkAddressType === 'town' ||\n checkAddressType === 'village'\n ) {\n return 'city';\n }\n\n if (\n checkType === 'state' ||\n checkType === 'province' ||\n checkType === 'region' ||\n checkAddressType === 'state'\n ) {\n return 'region';\n }\n\n if (checkType === 'country' || checkAddressType === 'country') {\n return 'country';\n }\n\n if (\n checkType === 'attraction' ||\n checkType === 'tourism' ||\n checkType === 'amenity'\n ) {\n return 'point_of_interest';\n }\n\n return 'unknown';\n}\n\n/**\n * Normalize a country code to uppercase ISO 3166-1 format.\n * Returns 'XX' for undefined, empty, or unrecognized inputs.\n *\n * @param code - Country code string (alpha-2 or alpha-3)\n * @returns Uppercase country code, or 'XX' if unknown\n */\nexport function normalizeCountryCode(code: string | undefined): string {\n if (!code) return 'XX'; // Unknown country code\n\n // Already uppercase 2-letter code\n const normalized = code.toUpperCase().trim();\n\n // If it's already 2 letters, return it\n if (normalized.length === 2) {\n return normalized;\n }\n\n // If it's 3 letters (alpha-3), we would need a mapping table\n // For now, return as-is or 'XX' for unknown\n return normalized.length === 3 ? normalized : 'XX';\n}\n\n/**\n * Validate that a latitude value is within the valid range (-90 to 90).\n *\n * @param lat - Latitude value to validate\n * @returns `true` if the value is a number between -90 and 90 inclusive\n */\nexport function isValidLatitude(lat: number): boolean {\n return typeof lat === 'number' && lat >= -90 && lat <= 90;\n}\n\n/**\n * Validate that a longitude value is within the valid range (-180 to 180).\n *\n * @param lng - Longitude value to validate\n * @returns `true` if the value is a number between -180 and 180 inclusive\n */\nexport function isValidLongitude(lng: number): boolean {\n return typeof lng === 'number' && lng >= -180 && lng <= 180;\n}\n\n/**\n * Validate that both latitude and longitude are within their valid ranges.\n *\n * @param latitude - Latitude value (-90 to 90)\n * @param longitude - Longitude value (-180 to 180)\n * @returns Object with `valid: true` if both are valid, or `valid: false` with an `error` message\n */\nexport function validateCoordinates(\n latitude: number,\n longitude: number,\n): { valid: boolean; error?: string } {\n if (!isValidLatitude(latitude)) {\n return {\n valid: false,\n error: `Invalid latitude: ${latitude}. Must be between -90 and 90.`,\n };\n }\n\n if (!isValidLongitude(longitude)) {\n return {\n valid: false,\n error: `Invalid longitude: ${longitude}. Must be between -180 and 180.`,\n };\n }\n\n return { valid: true };\n}\n","/**\n * IANA time zone resolution for geocoded coordinates.\n *\n * Offline lookups use `@photostructure/tz-lookup` (CC0-1.0, no runtime\n * dependencies, ~90 KB), a maintained fork of `tz-lookup` whose compressed\n * boundary table is regenerated from timezone-boundary-builder releases. It\n * answers in microseconds without network access, so every provider can fill\n * `Location.timezone` without extra billed requests or rate-limit pressure.\n *\n * The table trades precision for size: within a few kilometres of a zone\n * boundary it can pick the neighbouring zone. Callers that need exact answers\n * near borders can use the Google provider's `timezoneLookup: 'api'` mode.\n */\n\nimport tzLookup from '@photostructure/tz-lookup';\nimport type { Location } from './types';\nimport { validateCoordinates } from './utils';\n\n/**\n * How a provider fills `Location.timezone`.\n *\n * - `'offline'` (default): bundled boundary table, no network requests.\n * - `'api'`: provider time zone API (Google Time Zone API only), falling back\n * to the offline table when the API call fails.\n * - `'none'`: leave `timezone` unset.\n */\nexport type TimezoneLookupMode = 'offline' | 'api' | 'none';\n\n/**\n * Returns the IANA time zone for a coordinate using the bundled offline\n * boundary table, or `undefined` when the coordinate is invalid.\n *\n * @example\n * ```ts\n * timezoneForCoordinates(53.5461, -113.4938); // 'America/Edmonton'\n * ```\n */\nexport function timezoneForCoordinates(\n latitude: number,\n longitude: number,\n): string | undefined {\n if (!validateCoordinates(latitude, longitude).valid) {\n return undefined;\n }\n return tzLookup(latitude, longitude);\n}\n\n/**\n * Fills `timezone` on each location from the offline table, keeping any value\n * a provider already set. Returns the same array for chaining.\n */\nexport function withOfflineTimezones(locations: Location[]): Location[] {\n for (const location of locations) {\n if (location.timezone) continue;\n const timezone = timezoneForCoordinates(\n location.latitude,\n location.longitude,\n );\n if (timezone) {\n location.timezone = timezone;\n }\n }\n return locations;\n}\n","/**\n * Core types and interfaces for the Geo library\n */\n\n/**\n * Standardized location data structure\n */\nexport interface Location {\n /**\n * Unique identifier for the location (from provider)\n */\n id: string;\n\n /**\n * Type of location\n */\n type:\n | 'country'\n | 'region'\n | 'city'\n | 'address'\n | 'point_of_interest'\n | 'unknown';\n\n /**\n * Full formatted name/address of the location\n */\n name: string;\n\n /**\n * Latitude coordinate\n */\n latitude: number;\n\n /**\n * Longitude coordinate\n */\n longitude: number;\n\n /**\n * Address components (all optional)\n */\n addressComponents: {\n streetNumber?: string;\n streetName?: string;\n city?: string;\n region?: string;\n country?: string;\n postalCode?: string;\n };\n\n /**\n * ISO 3166-1 alpha-2 country code\n */\n countryCode: string;\n\n /**\n * IANA time zone identifier for the location's coordinates (for example\n * `'America/Edmonton'`). Filled by every provider unless the adapter was\n * created with `timezoneLookup: 'none'`; see `timezoneLookup` for sources.\n */\n timezone?: string;\n\n /**\n * Raw response from the provider (for debugging or provider-specific data)\n */\n raw: any;\n}\n\n/**\n * Options for POI (point-of-interest) searches.\n *\n * `types` and `keyword` are both forwarded to the backing provider but each\n * provider interprets them slightly differently:\n *\n * - **Google**: `types[0]` becomes the request's `type` filter (Places API\n * accepts a single type per request; additional entries are ignored).\n * `keyword` is a free-text match across name/type/address/reviews.\n * - **OpenStreetMap (Overpass)**: `types` are matched against\n * `amenity`, `shop`, and `tourism` tag values (e.g. `'cafe'`,\n * `'supermarket'`, `'museum'`). When omitted, the provider searches across\n * a broad set of POI-ish tag keys (`amenity`, `shop`, `tourism`,\n * `leisure`, `office`, `historic`). `keyword` is appended as a substring\n * filter on the `name` tag.\n */\nexport interface PoiSearchOptions {\n /** Filter results to POIs matching these category values. See notes above. */\n types?: string[];\n /** Free-text keyword to narrow the search. */\n keyword?: string;\n /** Max results to return. Default 20. */\n limit?: number;\n /** Preferred language for place names (Google only). */\n language?: string;\n}\n\n/**\n * Geo provider interface - all providers must implement lookup and\n * reverseGeocode. `findPoisNear` is optional — providers implement it when\n * they support POI discovery beyond reverse geocoding. Callers that need to\n * know whether a given instance supports POI search should feature-detect:\n *\n * ```ts\n * if (typeof adapter.findPoisNear === 'function') { ... }\n * ```\n */\nexport interface GeoProvider {\n /**\n * Look up locations based on a query string\n * @param query - Search string (address, city, country, POI, etc.)\n * @returns Promise resolving to array of matching Location objects\n */\n lookup(query: string): Promise<Location[]>;\n\n /**\n * Reverse geocode from coordinates to location\n * @param latitude - Latitude coordinate\n * @param longitude - Longitude coordinate\n * @returns Promise resolving to array of matching Location objects\n */\n reverseGeocode(latitude: number, longitude: number): Promise<Location[]>;\n\n /**\n * Find POIs (point-of-interest places — businesses, landmarks, amenities)\n * within a radius of a coordinate. Returns locations with\n * `type: 'point_of_interest'` whose coords lie inside the requested\n * radius, sorted by the provider's relevance ranking.\n *\n * @param latitude - Center latitude\n * @param longitude - Center longitude\n * @param radiusMeters - Search radius in meters\n * @param options - Optional filters\n * @returns Promise resolving to array of matching Location objects\n */\n findPoisNear?(\n latitude: number,\n longitude: number,\n radiusMeters: number,\n options?: PoiSearchOptions,\n ): Promise<Location[]>;\n}\n\n/**\n * Geo adapter interface (structurally identical to GeoProvider)\n */\nexport interface GeoAdapter {\n /**\n * Look up locations based on a query string\n * @param query - Search string (address, city, country, POI, etc.)\n * @returns Promise resolving to array of matching Location objects\n */\n lookup(query: string): Promise<Location[]>;\n\n /**\n * Reverse geocode from coordinates to location\n * @param latitude - Latitude coordinate\n * @param longitude - Longitude coordinate\n * @returns Promise resolving to array of matching Location objects\n */\n reverseGeocode(latitude: number, longitude: number): Promise<Location[]>;\n\n /**\n * Find POIs near a coordinate. Optional — see `GeoProvider.findPoisNear`.\n */\n findPoisNear?(\n latitude: number,\n longitude: number,\n radiusMeters: number,\n options?: PoiSearchOptions,\n ): Promise<Location[]>;\n}\n\n/**\n * Base configuration options for all providers\n */\nexport interface BaseGeoOptions {\n /**\n * Request timeout in milliseconds\n */\n timeout?: number;\n\n /**\n * Maximum number of results to return\n */\n maxResults?: number;\n\n /**\n * How to fill `Location.timezone`. `'offline'` (default) resolves it from a\n * bundled boundary table with no network requests; `'none'` leaves it\n * unset. The Google provider also accepts `'api'`.\n */\n timezoneLookup?: 'offline' | 'none';\n}\n\n/**\n * Google Maps provider options\n */\nexport interface GoogleMapsOptions\n extends Omit<BaseGeoOptions, 'timezoneLookup'> {\n provider: 'google';\n /**\n * Google Maps API key\n */\n apiKey: string;\n /**\n * How to fill `Location.timezone`. `'offline'` (default) uses the bundled\n * boundary table. `'api'` calls the Google Time Zone API (must be enabled\n * for the key; billed per request) once per distinct coordinate and falls\n * back to the offline table when that call fails. `'none'` leaves it unset.\n */\n timezoneLookup?: 'offline' | 'api' | 'none';\n}\n\n/**\n * OpenStreetMap provider options\n */\nexport interface OpenStreetMapOptions extends BaseGeoOptions {\n provider: 'openstreetmap';\n /**\n * Custom User-Agent for OSM requests (optional, defaults to package name)\n */\n userAgent?: string;\n /**\n * Rate limit delay in milliseconds (default: 1000ms for 1 req/sec)\n */\n rateLimitDelay?: number;\n}\n\n/**\n * Union type for all provider options\n */\nexport type GeoAdapterOptions = GoogleMapsOptions | OpenStreetMapOptions;\n\n/**\n * Base error class for geo operations.\n * All geo-specific errors extend this class and include a machine-readable `code`\n * and the `provider` name that produced the error.\n */\nexport class GeoError extends Error {\n /**\n * @param message - Human-readable error description\n * @param code - Machine-readable error code (e.g. 'RATE_LIMIT', 'AUTH_ERROR')\n * @param provider - Provider that raised the error ('google' | 'openstreetmap')\n */\n constructor(\n message: string,\n public code: string,\n public provider?: string,\n ) {\n super(message);\n this.name = 'GeoError';\n }\n}\n\n/**\n * Thrown when the provider's rate limit has been exceeded.\n */\nexport class RateLimitError extends GeoError {\n /**\n * @param provider - Provider that raised the error\n * @param retryAfter - Seconds to wait before retrying (if known)\n */\n constructor(provider?: string, retryAfter?: number) {\n super(\n `Rate limit exceeded${retryAfter ? `, retry after ${retryAfter}s` : ''}`,\n 'RATE_LIMIT',\n provider,\n );\n this.name = 'RateLimitError';\n }\n}\n\n/**\n * Thrown when the geocoding query is empty or malformed.\n */\nexport class InvalidQueryError extends GeoError {\n /**\n * @param query - The invalid query string\n * @param provider - Provider that raised the error\n */\n constructor(query: string, provider?: string) {\n super(`Invalid query: ${query}`, 'INVALID_QUERY', provider);\n this.name = 'InvalidQueryError';\n }\n}\n\n/**\n * Thrown when the provider rejects the API key or credentials.\n */\nexport class AuthenticationError extends GeoError {\n /**\n * @param provider - Provider that raised the error\n */\n constructor(provider?: string) {\n super('Authentication failed', 'AUTH_ERROR', provider);\n this.name = 'AuthenticationError';\n }\n}\n\n/**\n * Thrown when a geocoding query returns zero results.\n */\nexport class NoResultsError extends GeoError {\n /**\n * @param query - The query that produced no results\n * @param provider - Provider that raised the error\n */\n constructor(query: string, provider?: string) {\n super(`No results found for query: ${query}`, 'NO_RESULTS', provider);\n this.name = 'NoResultsError';\n }\n}\n","/**\n * Static Map Generation\n *\n * Generates static map URLs and fetches map images for location-based content.\n * Supports Mapbox and Google Maps providers.\n *\n * @example\n * ```typescript\n * import { getStaticMapUrl, fetchStaticMap } from '@happyvertical/geo';\n *\n * // Get URL for embedding\n * const url = getStaticMapUrl(53.5461, -113.4938, {\n * provider: 'mapbox',\n * zoom: 14,\n * width: 1200,\n * height: 630\n * });\n *\n * // Fetch the actual image\n * const buffer = await fetchStaticMap(53.5461, -113.4938, {\n * provider: 'mapbox'\n * });\n * await writeFile('map.png', buffer);\n * ```\n */\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Static map provider\n */\nexport type StaticMapProvider = 'mapbox' | 'google';\n\n/**\n * Mapbox map styles\n * @see https://docs.mapbox.com/api/maps/styles/\n */\nexport type MapboxStyle =\n | 'streets-v12'\n | 'outdoors-v12'\n | 'light-v11'\n | 'dark-v11'\n | 'satellite-v9'\n | 'satellite-streets-v12';\n\n/**\n * Google Maps map types\n * @see https://developers.google.com/maps/documentation/maps-static/start\n */\nexport type GoogleMapType = 'roadmap' | 'satellite' | 'terrain' | 'hybrid';\n\n/**\n * Marker definition for the map\n */\nexport interface StaticMapMarker {\n /** Latitude */\n latitude: number;\n /** Longitude */\n longitude: number;\n /** Marker color (hex or named color) */\n color?: string;\n /** Marker label (single character for Google, any for Mapbox) */\n label?: string;\n /** Marker size ('small' | 'medium' | 'large') */\n size?: 'small' | 'medium' | 'large';\n}\n\n/**\n * Options for static map generation\n */\nexport interface StaticMapOptions {\n /**\n * Map provider\n * @default 'mapbox'\n */\n provider?: StaticMapProvider;\n\n /**\n * Image width in pixels\n * @default 1200\n */\n width?: number;\n\n /**\n * Image height in pixels\n * @default 630\n */\n height?: number;\n\n /**\n * Zoom level (1-20)\n * @default 14\n */\n zoom?: number;\n\n /**\n * Marker color (hex without # or named color)\n * @default 'e74c3c' (red)\n */\n markerColor?: string;\n\n /**\n * Show a marker at the center point\n * @default true\n */\n showMarker?: boolean;\n\n /**\n * Mapbox access token (required for Mapbox provider)\n * Falls back to MAPBOX_ACCESS_TOKEN env var\n */\n mapboxToken?: string;\n\n /**\n * Google Maps API key (required for Google provider)\n * Falls back to GOOGLE_MAPS_API_KEY env var\n */\n googleApiKey?: string;\n\n /**\n * Mapbox style\n * @default 'streets-v12'\n */\n mapboxStyle?: MapboxStyle;\n\n /**\n * Google map type\n * @default 'roadmap'\n */\n googleMapType?: GoogleMapType;\n\n /**\n * Pixel density (1 or 2 for retina)\n * @default 1\n */\n scale?: 1 | 2;\n\n /**\n * Additional markers to display\n */\n markers?: StaticMapMarker[];\n}\n\n/**\n * Result from fetching a static map\n */\nexport interface StaticMapResult {\n /** PNG image buffer */\n buffer: Buffer;\n /** Image width */\n width: number;\n /** Image height */\n height: number;\n /** MIME type */\n mimeType: 'image/png';\n /** The URL that was fetched */\n url: string;\n}\n\n// ============================================================================\n// URL Generators\n// ============================================================================\n\n/**\n * Generate Mapbox static map URL\n */\nfunction getMapboxUrl(\n latitude: number,\n longitude: number,\n options: StaticMapOptions,\n): string {\n const token =\n options.mapboxToken ??\n process.env.MAPBOX_ACCESS_TOKEN ??\n process.env.MAPBOX_TOKEN;\n if (!token) {\n throw new Error(\n 'Mapbox access token required. Provide via options.mapboxToken or MAPBOX_ACCESS_TOKEN env var.',\n );\n }\n\n const width = options.width ?? 1200;\n const height = options.height ?? 630;\n const zoom = options.zoom ?? 14;\n const style = options.mapboxStyle ?? 'streets-v12';\n const scale = options.scale ?? 1;\n const showMarker = options.showMarker ?? true;\n\n // Build overlay for marker\n let overlay = '';\n if (showMarker) {\n const color = (options.markerColor ?? 'e74c3c').replace('#', '');\n // Mapbox marker format: pin-{size}-{label}+{color}({lon},{lat})\n overlay = `pin-l+${color}(${longitude},${latitude})/`;\n }\n\n // Add additional markers\n if (options.markers?.length) {\n const markerOverlays = options.markers.map((m) => {\n const color = (m.color ?? 'e74c3c').replace('#', '');\n const size = m.size === 'small' ? 's' : m.size === 'large' ? 'l' : 'm';\n const label = m.label ? `-${m.label}` : '';\n return `pin-${size}${label}+${color}(${m.longitude},${m.latitude})`;\n });\n overlay = markerOverlays.join(',') + '/';\n }\n\n const retinaStr = scale === 2 ? '@2x' : '';\n\n return `https://api.mapbox.com/styles/v1/mapbox/${style}/static/${overlay}${longitude},${latitude},${zoom}/${width}x${height}${retinaStr}?access_token=${token}`;\n}\n\n/**\n * Generate Google Maps static map URL\n */\nfunction getGoogleUrl(\n latitude: number,\n longitude: number,\n options: StaticMapOptions,\n): string {\n const apiKey =\n options.googleApiKey ??\n process.env.GOOGLE_MAPS_API_KEY ??\n process.env.GOOGLE_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'Google Maps API key required. Provide via options.googleApiKey or GOOGLE_MAPS_API_KEY env var.',\n );\n }\n\n const width = options.width ?? 1200;\n const height = options.height ?? 630;\n const zoom = options.zoom ?? 14;\n const mapType = options.googleMapType ?? 'roadmap';\n const scale = options.scale ?? 1;\n const showMarker = options.showMarker ?? true;\n\n const params = new URLSearchParams({\n center: `${latitude},${longitude}`,\n zoom: zoom.toString(),\n size: `${width}x${height}`,\n maptype: mapType,\n scale: scale.toString(),\n key: apiKey,\n });\n\n // Add center marker\n if (showMarker) {\n const color = (options.markerColor ?? 'red').replace('#', '0x');\n params.append('markers', `color:${color}|${latitude},${longitude}`);\n }\n\n // Add additional markers\n if (options.markers?.length) {\n for (const m of options.markers) {\n const color = (m.color ?? 'red').replace('#', '0x');\n const size = m.size ?? 'medium';\n const label = m.label ? `|label:${m.label.charAt(0).toUpperCase()}` : '';\n params.append(\n 'markers',\n `color:${color}|size:${size}${label}|${m.latitude},${m.longitude}`,\n );\n }\n }\n\n return `https://maps.googleapis.com/maps/api/staticmap?${params.toString()}`;\n}\n\n// ============================================================================\n// Main Functions\n// ============================================================================\n\n/**\n * Generate a static map URL\n *\n * Returns a URL that can be used directly in <img> tags or fetched for processing.\n *\n * @param latitude - Center latitude\n * @param longitude - Center longitude\n * @param options - Map options\n * @returns URL string for the static map\n *\n * @example Mapbox (default)\n * ```typescript\n * const url = getStaticMapUrl(53.5461, -113.4938, {\n * provider: 'mapbox',\n * zoom: 14,\n * mapboxStyle: 'streets-v12'\n * });\n * ```\n *\n * @example Google Maps\n * ```typescript\n * const url = getStaticMapUrl(53.5461, -113.4938, {\n * provider: 'google',\n * zoom: 14,\n * googleMapType: 'roadmap'\n * });\n * ```\n *\n * @example With custom markers\n * ```typescript\n * const url = getStaticMapUrl(53.5461, -113.4938, {\n * showMarker: false, // Hide center marker\n * markers: [\n * { latitude: 53.5461, longitude: -113.4938, color: 'blue', label: 'A' },\n * { latitude: 53.5500, longitude: -113.4900, color: 'green', label: 'B' }\n * ]\n * });\n * ```\n */\nexport function getStaticMapUrl(\n latitude: number,\n longitude: number,\n options: StaticMapOptions = {},\n): string {\n const provider = options.provider ?? 'mapbox';\n\n switch (provider) {\n case 'google':\n return getGoogleUrl(latitude, longitude, options);\n case 'mapbox':\n default:\n return getMapboxUrl(latitude, longitude, options);\n }\n}\n\n/**\n * Fetch a static map image\n *\n * Downloads the map image and returns it as a Buffer.\n *\n * @param latitude - Center latitude\n * @param longitude - Center longitude\n * @param options - Map options\n * @returns Promise resolving to the map image result\n *\n * @example\n * ```typescript\n * const result = await fetchStaticMap(53.5461, -113.4938, {\n * provider: 'mapbox',\n * width: 1200,\n * height: 630\n * });\n *\n * await fs.writeFile('map.png', result.buffer);\n * console.log(`Fetched ${result.width}x${result.height} map`);\n * ```\n */\nexport async function fetchStaticMap(\n latitude: number,\n longitude: number,\n options: StaticMapOptions = {},\n): Promise<StaticMapResult> {\n const width = options.width ?? 1200;\n const height = options.height ?? 630;\n const url = getStaticMapUrl(latitude, longitude, options);\n\n const response = await fetch(url);\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Failed to fetch static map: ${response.status} ${response.statusText} - ${errorText}`,\n );\n }\n\n const arrayBuffer = await response.arrayBuffer();\n const buffer = Buffer.from(arrayBuffer);\n\n return {\n buffer,\n width,\n height,\n mimeType: 'image/png',\n url,\n };\n}\n\n/**\n * Generate OG-sized static map URL (1200x630)\n *\n * Convenience function for generating Open Graph / social media sized maps.\n *\n * @param latitude - Center latitude\n * @param longitude - Center longitude\n * @param options - Map options (width/height ignored, set to 1200x630)\n * @returns URL string for the static map\n *\n * @example\n * ```typescript\n * const url = getOGMapUrl(53.5461, -113.4938);\n * // Returns 1200x630 map URL\n * ```\n */\nexport function getOGMapUrl(\n latitude: number,\n longitude: number,\n options: Omit<StaticMapOptions, 'width' | 'height'> = {},\n): string {\n return getStaticMapUrl(latitude, longitude, {\n ...options,\n width: 1200,\n height: 630,\n });\n}\n","/**\n * Geo package entry point\n * Provides standardized geographical information interface\n */\n\nimport { loadEnvConfig } from '@happyvertical/utils';\nimport type {\n GeoAdapter,\n GeoAdapterOptions,\n GoogleMapsOptions,\n OpenStreetMapOptions,\n} from './shared/types';\n\n// Export all types\nexport * from './shared/timezone';\nexport * from './shared/types';\nexport * from './shared/utils';\n\n// Export static map generation\nexport {\n fetchStaticMap,\n type GoogleMapType,\n getOGMapUrl,\n getStaticMapUrl,\n type MapboxStyle,\n type StaticMapMarker,\n type StaticMapOptions,\n type StaticMapProvider,\n type StaticMapResult,\n} from './static-maps';\n\n/**\n * Type guard for Google Maps options\n */\nfunction isGoogleMapsOptions(\n options: GeoAdapterOptions,\n): options is GoogleMapsOptions {\n return options.provider === 'google';\n}\n\n/**\n * Type guard for OpenStreetMap options\n */\nfunction isOpenStreetMapOptions(\n options: GeoAdapterOptions,\n): options is OpenStreetMapOptions {\n return options.provider === 'openstreetmap';\n}\n\n/**\n * Factory function to create a geo adapter instance\n *\n * Supports configuration via environment variables using the pattern:\n * - HAVE_GEO_PROVIDER → provider ('google' | 'openstreetmap')\n * - GOOGLE_MAPS_API_KEY → apiKey (for Google Maps provider)\n *\n * User-provided options always take precedence over environment variables.\n *\n * @param options - Configuration options for the geo provider (optional)\n * @returns Promise resolving to a geo adapter that implements GeoAdapter\n *\n * @example\n * ```typescript\n * // Create Google Maps adapter with explicit options\n * const googleGeo = await getGeoAdapter({\n * provider: 'google',\n * apiKey: process.env.GOOGLE_MAPS_API_KEY!\n * });\n *\n * // Create adapter using environment variables\n * // HAVE_GEO_PROVIDER=google\n * // GOOGLE_MAPS_API_KEY=your-api-key\n * const geoFromEnv = await getGeoAdapter();\n *\n * // Create OpenStreetMap adapter\n * const osmGeo = await getGeoAdapter({\n * provider: 'openstreetmap'\n * });\n *\n * // Use the adapter\n * const locations = await googleGeo.lookup('Eiffel Tower');\n * const coords = await osmGeo.reverseGeocode(48.8584, 2.2945);\n * ```\n */\nexport async function getGeoAdapter(\n options: Partial<GeoAdapterOptions> = {},\n): Promise<GeoAdapter> {\n // Load configuration from environment variables\n // Cast to any to handle union type with loadEnvConfig\n const config = loadEnvConfig(options as any, {\n packageName: 'geo',\n schema: {\n provider: 'string',\n timeout: 'number',\n maxResults: 'number',\n rateLimitDelay: 'number',\n userAgent: 'string',\n timezoneLookup: 'string',\n },\n }) as Partial<GeoAdapterOptions>;\n\n // Handle Google Maps API key from environment (not using HAVE_GEO_ prefix)\n if (config.provider === 'google' && !config.apiKey) {\n const apiKey = process.env.GOOGLE_MAPS_API_KEY;\n if (apiKey) {\n config.apiKey = apiKey;\n }\n }\n\n // Validate that we have a provider\n if (!config.provider) {\n throw new Error(\n 'Provider is required. Set via options.provider or HAVE_GEO_PROVIDER environment variable.',\n );\n }\n\n // Cast to full options type for provider-specific handling\n const fullOptions = config as GeoAdapterOptions;\n\n if (isGoogleMapsOptions(fullOptions)) {\n const { GoogleMapsProvider } = await import('./providers/google.js');\n return new GoogleMapsProvider(fullOptions);\n }\n\n if (isOpenStreetMapOptions(fullOptions)) {\n const { OpenStreetMapProvider } = await import(\n './providers/openstreetmap.js'\n );\n return new OpenStreetMapProvider(fullOptions);\n }\n\n // This should never happen due to TypeScript's discriminated union\n throw new Error(`Unsupported provider: ${(fullOptions as any).provider}`);\n}\n\n/** @internal */\nexport const PACKAGE_VERSION_INITIALIZED = true;\n"],"mappings":"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"}
@@ -8,15 +8,37 @@ export declare class GoogleMapsProvider implements GeoProvider {
8
8
  private timeout;
9
9
  private maxResults;
10
10
  private cache;
11
+ private timezoneLookup;
12
+ private warnedTimezoneFallback;
11
13
  constructor(options: GoogleMapsOptions);
12
14
  /**
13
15
  * Initializes the memory cache for geocoding results
14
16
  */
15
17
  private initCache;
16
18
  /**
17
- * Generates a cache key for geocoding requests
19
+ * Generates a cache key for geocoding requests. The time zone mode is part
20
+ * of the key so adapters with different modes never share results.
18
21
  */
19
22
  private getCacheKey;
23
+ /**
24
+ * Fills `Location.timezone` according to the configured mode. In `'api'`
25
+ * mode the Google Time Zone API is called once per distinct coordinate,
26
+ * concurrently; a failed or non-OK call falls back to the offline table
27
+ * for that point.
28
+ */
29
+ private applyTimezones;
30
+ /**
31
+ * Resolves a coordinate's IANA zone through the Google Time Zone API,
32
+ * falling back to the offline table when the API is unavailable, denied,
33
+ * or returns no zone.
34
+ */
35
+ private fetchTimezone;
36
+ /**
37
+ * Warns once per adapter that Time Zone API lookups are falling back to
38
+ * the offline table. Logs only a short reason, never the error, request,
39
+ * or key.
40
+ */
41
+ private warnTimezoneFallback;
20
42
  /**
21
43
  * Look up locations based on a query string
22
44
  */
@@ -1 +1 @@
1
- {"version":3,"file":"google.d.ts","sourceRoot":"","sources":["../../src/providers/google.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EACV,WAAW,EACX,iBAAiB,EACjB,QAAQ,EACR,gBAAgB,EACjB,MAAM,iBAAiB,CAAC;AAazB;;GAEG;AACH,qBAAa,kBAAmB,YAAW,WAAW;IACpD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,KAAK,CAA6B;gBAE9B,OAAO,EAAE,iBAAiB;IAUtC;;OAEG;YACW,SAAS;IAgBvB;;OAEG;IACH,OAAO,CAAC,WAAW;IAInB;;OAEG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAmEhD;;OAEG;IACG,cAAc,CAClB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,QAAQ,EAAE,CAAC;IA4EtB;;;;;;;OAOG;IACG,YAAY,CAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,QAAQ,EAAE,CAAC;IAgItB;;;;;OAKG;IACH,OAAO,CAAC,sBAAsB;IAsB9B;;OAEG;IACH,OAAO,CAAC,yBAAyB;CAgDlC"}
1
+ {"version":3,"file":"google.d.ts","sourceRoot":"","sources":["../../src/providers/google.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,OAAO,KAAK,EACV,WAAW,EACX,iBAAiB,EACjB,QAAQ,EACR,gBAAgB,EACjB,MAAM,iBAAiB,CAAC;AAazB;;GAEG;AACH,qBAAa,kBAAmB,YAAW,WAAW;IACpD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,sBAAsB,CAAS;gBAE3B,OAAO,EAAE,iBAAiB;IActC;;OAEG;YACW,SAAS;IAgBvB;;;OAGG;IACH,OAAO,CAAC,WAAW;IAInB;;;;;OAKG;YACW,cAAc;IA4B5B;;;;OAIG;YACW,aAAa;IA0B3B;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAQ5B;;OAEG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAmEhD;;OAEG;IACG,cAAc,CAClB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,QAAQ,EAAE,CAAC;IA4EtB;;;;;;;OAOG;IACG,YAAY,CAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,QAAQ,EAAE,CAAC;IAgItB;;;;;OAKG;IACH,OAAO,CAAC,sBAAsB;IAsB9B;;OAEG;IACH,OAAO,CAAC,yBAAyB;CAgDlC"}
@@ -16,15 +16,21 @@ export declare class OpenStreetMapProvider implements GeoProvider {
16
16
  private timeout;
17
17
  private maxResults;
18
18
  private cache;
19
+ private timezoneLookup;
19
20
  constructor(options: OpenStreetMapOptions);
20
21
  /**
21
22
  * Initializes the memory cache for geocoding results
22
23
  */
23
24
  private initCache;
24
25
  /**
25
- * Generates a cache key for geocoding requests
26
+ * Generates a cache key for geocoding requests. The time zone mode is part
27
+ * of the key so adapters with different modes never share results.
26
28
  */
27
29
  private getCacheKey;
30
+ /**
31
+ * Fills `Location.timezone` according to the configured mode.
32
+ */
33
+ private applyTimezones;
28
34
  /**
29
35
  * Enforce rate limiting by waiting if necessary
30
36
  */
@@ -1 +1 @@
1
- {"version":3,"file":"openstreetmap.d.ts","sourceRoot":"","sources":["../../src/providers/openstreetmap.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EACR,oBAAoB,EACpB,gBAAgB,EACjB,MAAM,iBAAiB,CAAC;AA2EzB;;GAEG;AACH,qBAAa,qBAAsB,YAAW,WAAW;IACvD,OAAO,CAAC,OAAO,CAAyC;IACxD;;;;OAIG;IACH,OAAO,CAAC,WAAW,CAA6C;IAChE,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,KAAK,CAA6B;gBAE9B,OAAO,EAAE,oBAAoB;IAUzC;;OAEG;YACW,SAAS;IAgBvB;;OAEG;IACH,OAAO,CAAC,WAAW;IAInB;;OAEG;YACW,gBAAgB;IAY9B;;OAEG;YACW,cAAc;IAkE5B;;OAEG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAuChD;;OAEG;IACG,cAAc,CAClB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,QAAQ,EAAE,CAAC;IAqDtB;;;;OAIG;IACG,YAAY,CAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,QAAQ,EAAE,CAAC;IA0GtB;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAwC1B;;;;OAIG;IACH,OAAO,CAAC,4BAA4B;IAmCpC;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IAQ9B;;OAEG;IACH,OAAO,CAAC,4BAA4B;CA2CrC"}
1
+ {"version":3,"file":"openstreetmap.d.ts","sourceRoot":"","sources":["../../src/providers/openstreetmap.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EACR,oBAAoB,EACpB,gBAAgB,EACjB,MAAM,iBAAiB,CAAC;AA2EzB;;GAEG;AACH,qBAAa,qBAAsB,YAAW,WAAW;IACvD,OAAO,CAAC,OAAO,CAAyC;IACxD;;;;OAIG;IACH,OAAO,CAAC,WAAW,CAA6C;IAChE,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,cAAc,CAAqB;gBAE/B,OAAO,EAAE,oBAAoB;IAczC;;OAEG;YACW,SAAS;IAgBvB;;;OAGG;IACH,OAAO,CAAC,WAAW;IAInB;;OAEG;IACH,OAAO,CAAC,cAAc;IAMtB;;OAEG;YACW,gBAAgB;IAY9B;;OAEG;YACW,cAAc;IAkE5B;;OAEG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAuChD;;OAEG;IACG,cAAc,CAClB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,QAAQ,EAAE,CAAC;IAqDtB;;;;OAIG;IACG,YAAY,CAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,QAAQ,EAAE,CAAC;IA2GtB;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAwC1B;;;;OAIG;IACH,OAAO,CAAC,4BAA4B;IAmCpC;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IAQ9B;;OAEG;IACH,OAAO,CAAC,4BAA4B;CA2CrC"}
@@ -0,0 +1,13 @@
1
+ import { TimezoneLookupMode } from './timezone';
2
+ /**
3
+ * Returns a valid `timezoneLookup` mode for a provider, defaulting to
4
+ * `'offline'`. Values are trimmed and case-insensitive (environment variables
5
+ * arrive as strings). Anything else throws a `GeoError` naming the accepted
6
+ * values, rather than silently disabling or changing the lookup.
7
+ *
8
+ * `'api'` exists only for Google; OpenStreetMap has no time zone API, so it
9
+ * warns once per process and uses `'offline'`.
10
+ */
11
+ export declare function resolveTimezoneLookup(value: unknown, provider: 'google'): TimezoneLookupMode;
12
+ export declare function resolveTimezoneLookup(value: unknown, provider: 'openstreetmap'): 'offline' | 'none';
13
+ //# sourceMappingURL=timezone-mode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timezone-mode.d.ts","sourceRoot":"","sources":["../../src/shared/timezone-mode.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAOrD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,QAAQ,GACjB,kBAAkB,CAAC;AACtB,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,eAAe,GACxB,SAAS,GAAG,MAAM,CAAC"}
@@ -0,0 +1,26 @@
1
+ import { Location } from './types';
2
+ /**
3
+ * How a provider fills `Location.timezone`.
4
+ *
5
+ * - `'offline'` (default): bundled boundary table, no network requests.
6
+ * - `'api'`: provider time zone API (Google Time Zone API only), falling back
7
+ * to the offline table when the API call fails.
8
+ * - `'none'`: leave `timezone` unset.
9
+ */
10
+ export type TimezoneLookupMode = 'offline' | 'api' | 'none';
11
+ /**
12
+ * Returns the IANA time zone for a coordinate using the bundled offline
13
+ * boundary table, or `undefined` when the coordinate is invalid.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * timezoneForCoordinates(53.5461, -113.4938); // 'America/Edmonton'
18
+ * ```
19
+ */
20
+ export declare function timezoneForCoordinates(latitude: number, longitude: number): string | undefined;
21
+ /**
22
+ * Fills `timezone` on each location from the offline table, keeping any value
23
+ * a provider already set. Returns the same array for chaining.
24
+ */
25
+ export declare function withOfflineTimezones(locations: Location[]): Location[];
26
+ //# sourceMappingURL=timezone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timezone.d.ts","sourceRoot":"","sources":["../../src/shared/timezone.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGxC;;;;;;;GAOG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,KAAK,GAAG,MAAM,CAAC;AAE5D;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAKpB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,QAAQ,EAAE,CAYtE"}
@@ -41,7 +41,9 @@ export interface Location {
41
41
  */
42
42
  countryCode: string;
43
43
  /**
44
- * Timezone identifier (optional, populated when provider returns it)
44
+ * IANA time zone identifier for the location's coordinates (for example
45
+ * `'America/Edmonton'`). Filled by every provider unless the adapter was
46
+ * created with `timezoneLookup: 'none'`; see `timezoneLookup` for sources.
45
47
  */
46
48
  timezone?: string;
47
49
  /**
@@ -147,16 +149,29 @@ export interface BaseGeoOptions {
147
149
  * Maximum number of results to return
148
150
  */
149
151
  maxResults?: number;
152
+ /**
153
+ * How to fill `Location.timezone`. `'offline'` (default) resolves it from a
154
+ * bundled boundary table with no network requests; `'none'` leaves it
155
+ * unset. The Google provider also accepts `'api'`.
156
+ */
157
+ timezoneLookup?: 'offline' | 'none';
150
158
  }
151
159
  /**
152
160
  * Google Maps provider options
153
161
  */
154
- export interface GoogleMapsOptions extends BaseGeoOptions {
162
+ export interface GoogleMapsOptions extends Omit<BaseGeoOptions, 'timezoneLookup'> {
155
163
  provider: 'google';
156
164
  /**
157
165
  * Google Maps API key
158
166
  */
159
167
  apiKey: string;
168
+ /**
169
+ * How to fill `Location.timezone`. `'offline'` (default) uses the bundled
170
+ * boundary table. `'api'` calls the Google Time Zone API (must be enabled
171
+ * for the key; billed per request) once per distinct coordinate and falls
172
+ * back to the offline table when that call fails. `'none'` leaves it unset.
173
+ */
174
+ timezoneLookup?: 'offline' | 'api' | 'none';
160
175
  }
161
176
  /**
162
177
  * OpenStreetMap provider options
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB;;OAEG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,IAAI,EACA,SAAS,GACT,QAAQ,GACR,MAAM,GACN,SAAS,GACT,mBAAmB,GACnB,SAAS,CAAC;IAEd;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,iBAAiB,EAAE;QACjB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IAEF;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,GAAG,EAAE,GAAG,CAAC;CACV;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAE3C;;;;;OAKG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAEzE;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,CACX,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAE3C;;;;;OAKG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAEzE;;OAEG;IACH,YAAY,CAAC,CACX,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACvD,QAAQ,EAAE,QAAQ,CAAC;IACnB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAqB,SAAQ,cAAc;IAC1D,QAAQ,EAAE,eAAe,CAAC;IAC1B;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,GAAG,oBAAoB,CAAC;AAEzE;;;;GAIG;AACH,qBAAa,QAAS,SAAQ,KAAK;IAQxB,IAAI,EAAE,MAAM;IACZ,QAAQ,CAAC,EAAE,MAAM;IAR1B;;;;OAIG;gBAED,OAAO,EAAE,MAAM,EACR,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,YAAA;CAK3B;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,QAAQ;IAC1C;;;OAGG;gBACS,QAAQ,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAQnD;AAED;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,QAAQ;IAC7C;;;OAGG;gBACS,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM;CAI7C;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,QAAQ;IAC/C;;OAEG;gBACS,QAAQ,CAAC,EAAE,MAAM;CAI9B;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,QAAQ;IAC1C;;;OAGG;gBACS,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM;CAI7C"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"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"}
package/metadata.json CHANGED
@@ -17,7 +17,8 @@
17
17
  ],
18
18
  "externalHappyVertical": [],
19
19
  "external": [
20
- "@googlemaps/google-maps-services-js"
20
+ "@googlemaps/google-maps-services-js",
21
+ "@photostructure/tz-lookup"
21
22
  ]
22
23
  },
23
24
  "dependents": [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/geo",
3
- "version": "0.96.1",
3
+ "version": "0.98.0",
4
4
  "description": "Standardized geographical information interface supporting Google Maps and OpenStreetMap",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -46,8 +46,9 @@
46
46
  "license": "ISC",
47
47
  "dependencies": {
48
48
  "@googlemaps/google-maps-services-js": "^3.4.2",
49
- "@happyvertical/cache": "0.96.1",
50
- "@happyvertical/utils": "0.96.1"
49
+ "@happyvertical/cache": "0.98.0",
50
+ "@happyvertical/utils": "0.98.0",
51
+ "@photostructure/tz-lookup": "^11.7.0"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@types/node": "25.0.10",
@@ -1 +0,0 @@
1
- {"version":3,"file":"google-CZUa1ho0.js","names":[],"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"],"mappings":";;;;;;;;;;AA4BA,IAAa,qBAAb,MAAuD;CACrD;CACA;CACA;CACA;CACA,QAAqC;CAErC,YAAY,SAA4B;EACtC,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC;EAC3B,KAAK,SAAS,QAAQ;EACtB,KAAK,UAAU,QAAQ,WAAW;EAClC,KAAK,aAAa,QAAQ,cAAc;EAGxC,KAAK,UAAU;CACjB;;;;CAKA,MAAc,YAA2B;EACvC,IAAI;GACF,KAAK,QAAQ,MAAM,SAAS;IAC1B,UAAU;IACV,WAAW;IACX,YAAY;IACZ,SAAS,KAAK,OAAO;IACrB,YAAY;IACZ,gBAAgB;GAClB,CAAC;EACH,SAAS,OAAO;GAEd,QAAQ,KAAK,mCAAmC,KAAK;EACvD;CACF;;;;CAKA,YAAoB,MAAc,GAAG,OAAyB;EAC5D,OAAO,GAAG,KAAK,GAAG,MAAM,KAAK,GAAG;CAClC;;;;CAKA,MAAM,OAAO,OAAoC;EAC/C,IAAI,CAAC,SAAS,MAAM,KAAK,CAAC,CAAC,WAAW,GACpC,MAAM,IAAI,kBAAkB,OAAO,QAAQ;EAI7C,MAAM,WAAW,KAAK,YAAY,UAAU,OAAO,OAAO,KAAK,UAAU,CAAC;EAC1E,IAAI,KAAK,OAAO;GACd,MAAM,SAAS,MAAM,KAAK,MAAM,IAAgB,QAAQ;GACxD,IAAI,QACF,OAAO;EAEX;EAEA,IAAI;GACF,MAAM,WAAW,MAAM,KAAK,OAAO,QAAQ;IACzC,QAAQ;KACN,SAAS;KACT,KAAK,KAAK;IACZ;IACA,SAAS,KAAK;GAChB,CAAC;GAED,IAAI,SAAS,KAAK,WAAW,gBAC3B,OAAO,CAAC;GAGV,IAAI,SAAS,KAAK,WAAW,kBAC3B,MAAM,IAAI,oBAAoB,QAAQ;GAGxC,IAAI,SAAS,KAAK,WAAW,oBAC3B,MAAM,IAAI,eAAe,QAAQ;GAGnC,IAAI,SAAS,KAAK,WAAW,MAC3B,MAAM,IAAI,SACR,0BAA0B,SAAS,KAAK,UACxC,aACA,QACF;GAIF,MAAM,YADU,SAAS,KAAK,QAAQ,MAAM,GAAG,KAAK,UAClC,CAAA,CAAQ,KAAK,WAC7B,KAAK,0BAA0B,MAAM,CACvC;GAGA,IAAI,KAAK,OACP,MAAM,KAAK,MAAM,IAAI,UAAU,SAAS;GAG1C,OAAO;EACT,SAAS,OAAO;GACd,IAAI,iBAAiB,UACnB,MAAM;GAGR,MAAM,IAAI,SACR,8BAA+B,MAAgB,WAC/C,iBACA,QACF;EACF;CACF;;;;CAKA,MAAM,eACJ,UACA,WACqB;EACrB,MAAM,aAAa,oBAAoB,UAAU,SAAS;EAC1D,IAAI,CAAC,WAAW,OACd,MAAM,IAAI,kBACR,GAAG,SAAS,IAAI,UAAU,IAAI,WAAW,SACzC,QACF;EAIF,MAAM,WAAW,KAAK,YACpB,WACA,OAAO,QAAQ,GACf,OAAO,SAAS,GAChB,OAAO,KAAK,UAAU,CACxB;EACA,IAAI,KAAK,OAAO;GACd,MAAM,SAAS,MAAM,KAAK,MAAM,IAAgB,QAAQ;GACxD,IAAI,QACF,OAAO;EAEX;EAEA,IAAI;GACF,MAAM,WAAW,MAAM,KAAK,OAAO,eAAe;IAChD,QAAQ;KACN,QAAQ;MAAE,KAAK;MAAU,KAAK;KAAU;KACxC,KAAK,KAAK;IACZ;IACA,SAAS,KAAK;GAChB,CAAC;GAED,IAAI,SAAS,KAAK,WAAW,gBAC3B,OAAO,CAAC;GAGV,IAAI,SAAS,KAAK,WAAW,kBAC3B,MAAM,IAAI,oBAAoB,QAAQ;GAGxC,IAAI,SAAS,KAAK,WAAW,oBAC3B,MAAM,IAAI,eAAe,QAAQ;GAGnC,IAAI,SAAS,KAAK,WAAW,MAC3B,MAAM,IAAI,SACR,0BAA0B,SAAS,KAAK,UACxC,aACA,QACF;GAIF,MAAM,YADU,SAAS,KAAK,QAAQ,MAAM,GAAG,KAAK,UAClC,CAAA,CAAQ,KAAK,WAC7B,KAAK,0BAA0B,MAAM,CACvC;GAGA,IAAI,KAAK,OACP,MAAM,KAAK,MAAM,IAAI,UAAU,SAAS;GAG1C,OAAO;EACT,SAAS,OAAO;GACd,IAAI,iBAAiB,UACnB,MAAM;GAGR,MAAM,IAAI,SACR,8BAA+B,MAAgB,WAC/C,0BACA,QACF;EACF;CACF;;;;;;;;;CAUA,MAAM,aACJ,UACA,WACA,cACA,UAA4B,CAAC,GACR;EACrB,MAAM,aAAa,oBAAoB,UAAU,SAAS;EAC1D,IAAI,CAAC,WAAW,OACd,MAAM,IAAI,kBACR,GAAG,SAAS,IAAI,UAAU,IAAI,WAAW,SACzC,QACF;EAEF,IAAI,EAAE,eAAe,MAAM,eAAe,KACxC,MAAM,IAAI,kBACR,UAAU,aAAa,0BACvB,QACF;EAGF,MAAM,QAAQ,QAAQ,SAAS,KAAK;EACpC,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,GACtC,MAAM,IAAI,kBACR,SAAS,MAAM,8BACf,QACF;EAEF,MAAM,QACJ,QAAQ,SAAS,QAAQ,MAAM,SAAS,IAAI,QAAQ,QAAQ,CAAC,KAAA,CAAS;EAExE,MAAM,WAAW,KAAK,YACpB,QACA,OAAO,QAAQ,GACf,OAAO,SAAS,GAChB,OAAO,YAAY,IAClB,QAAQ,SAAS,CAAC,EAAA,CAAG,KAAK,GAAG,GAC9B,QAAQ,WAAW,IACnB,QAAQ,YAAY,IACpB,OAAO,KAAK,CACd;EACA,IAAI,KAAK,OAAO;GACd,MAAM,SAAS,MAAM,KAAK,MAAM,IAAgB,QAAQ;GACxD,IAAI,QAAQ,OAAO;EACrB;EAEA,IAAI;GACF,MAAM,yBAAS,IAAI,IAAsB;GACzC,KAAK,MAAM,QAAQ,OAAO;IAMxB,IAAI;IACJ,IAAI,eAAe;IACnB,MAAM,kBAAkB;IAExB,OAAO,eAAe,iBAAiB;KACrC,MAAM,SAAS,YACX;MAAE,WAAW;MAAW,KAAK,KAAK;KAAO,IACzC;MACE,UAAU;OAAE,KAAK;OAAU,KAAK;MAAU;MAC1C,QAAQ;MACR,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;MACvB,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;MACtD,GAAI,QAAQ,WACR,EAAE,UAAU,QAAQ,SAAgB,IACpC,CAAC;MACL,KAAK,KAAK;KACZ;KAEJ,MAAM,WAAW,MAAM,KAAK,OAAO,aAAa;MAC9C;MACA,SAAS,KAAK;KAChB,CAAC;KAED,IAAI,SAAS,KAAK,WAAW,gBAAgB;KAC7C,IAAI,SAAS,KAAK,WAAW,kBAC3B,MAAM,IAAI,oBAAoB,QAAQ;KAExC,IAAI,SAAS,KAAK,WAAW,oBAC3B,MAAM,IAAI,eAAe,QAAQ;KAEnC,IAAI,SAAS,KAAK,WAAW,qBAAqB,WAAW;MAI3D,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,GAAI,CAAC;MACxD;KACF;KACA,IAAI,SAAS,KAAK,WAAW,MAC3B,MAAM,IAAI,SACR,4BAA4B,SAAS,KAAK,UAC1C,aACA,QACF;KAGF,KAAK,MAAM,UAAU,SAAS,KAAK,SAAS;MAC1C,MAAM,WAAW,KAAK,uBAAuB,MAAM;MACnD,IAAI,CAAC,OAAO,IAAI,SAAS,EAAE,GACzB,OAAO,IAAI,SAAS,IAAI,QAAQ;MAElC,IAAI,OAAO,QAAQ,OAAO;KAC5B;KAEA,gBAAgB;KAChB,IAAI,OAAO,QAAQ,OAAO;KAE1B,YAAY,SAAS,KAAK;KAC1B,IAAI,CAAC,WAAW;KAIhB,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,GAAI,CAAC;IAC1D;IAEA,IAAI,OAAO,QAAQ,OAAO;GAC5B;GAEA,MAAM,YAAY,CAAC,GAAG,OAAO,OAAO,CAAC;GACrC,IAAI,KAAK,OAAO,MAAM,KAAK,MAAM,IAAI,UAAU,SAAS;GACxD,OAAO;EACT,SAAS,OAAO;GACd,IAAI,iBAAiB,UAAU,MAAM;GACrC,MAAM,IAAI,SACR,wBAAyB,MAAgB,WACzC,qBACA,QACF;EACF;CACF;;;;;;;CAQA,uBAA+B,QAAuB;EACpD,MAAM,MAAM,OAAO,UAAU;EAC7B,MAAM,WAAW,KAAK,OAAO;EAC7B,MAAM,YAAY,KAAK,OAAO;EAC9B,MAAM,OAAO,OAAO,OAChB,OAAO,WACL,GAAG,OAAO,KAAK,IAAI,OAAO,aAC1B,OAAO,OACT,OAAO,YAAY;EAEvB,OAAO;GACL,IAAI,OAAO;GACX,MAAM;GACN;GACA;GACA;GACA,mBAAmB,CAAC;GACpB,aAAa;GACb,KAAK;EACP;CACF;;;;CAKA,0BAAkC,QAAuB;EAEvD,MAAM,oBAAmD,CAAC;EAC1D,IAAI,cAAc;EAElB,KAAK,MAAM,aAAa,OAAO,sBAAsB,CAAC,GAAG;GACvD,MAAM,QAAQ,UAAU;GAExB,IAAI,MAAM,SAAS,eAAe,GAChC,kBAAkB,eAAe,UAAU;GAE7C,IAAI,MAAM,SAAS,OAAO,GACxB,kBAAkB,aAAa,UAAU;GAE3C,IAAI,MAAM,SAAS,UAAU,GAC3B,kBAAkB,OAAO,UAAU;GAErC,IAAI,MAAM,SAAS,6BAA6B,GAC9C,kBAAkB,SAAS,UAAU;GAEvC,IAAI,MAAM,SAAS,SAAS,GAAG;IAC7B,kBAAkB,UAAU,UAAU;IACtC,cAAc,UAAU;GAC1B;GACA,IAAI,MAAM,SAAS,aAAa,GAC9B,kBAAkB,aAAa,UAAU;EAE7C;EAGA,MAAM,WAAW,OAAO,UAAU;EAClC,MAAM,WAAW,UAAU,OAAO;EAClC,MAAM,YAAY,UAAU,OAAO;EAGnC,MAAM,OAAO,mBAAmB,OAAO,SAAS,CAAC,CAAC;EAElD,OAAO;GACL,IAAI,OAAO;GACX;GACA,MAAM,OAAO;GACb;GACA;GACA;GACA,aAAa,qBAAqB,WAAW;GAC7C,KAAK;EACP;CACF;AACF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"openstreetmap-St4Kw-vS.js","names":[],"sources":["../../src/providers/openstreetmap.ts"],"sourcesContent":["/**\n * OpenStreetMap (Nominatim) provider implementation\n */\n\nimport type { CacheAdapter } from '@happyvertical/cache';\nimport { getCache } from '@happyvertical/cache';\nimport type {\n GeoProvider,\n Location,\n OpenStreetMapOptions,\n PoiSearchOptions,\n} from '../shared/types';\nimport { GeoError, InvalidQueryError, RateLimitError } from '../shared/types';\nimport {\n mapOSMPlaceType,\n normalizeCountryCode,\n validateCoordinates,\n} from '../shared/utils';\n\n/**\n * OpenStreetMap Nominatim API response structure\n */\ninterface NominatimResult {\n place_id: number;\n licence: string;\n osm_type: string;\n osm_id: number;\n lat: string;\n lon: string;\n display_name: string;\n address?: {\n house_number?: string;\n road?: string;\n city?: string;\n town?: string;\n village?: string;\n state?: string;\n country?: string;\n postcode?: string;\n country_code?: string;\n };\n type?: string;\n addresstype?: string;\n boundingbox?: string[];\n [key: string]: any;\n}\n\n/**\n * Overpass API response element (node or way within the result set).\n */\ninterface OverpassElement {\n type: 'node' | 'way' | 'relation';\n id: number;\n lat?: number;\n lon?: number;\n center?: { lat: number; lon: number };\n tags?: Record<string, string>;\n}\n\n/**\n * Escape a literal string for safe interpolation into an Overpass\n * `name~\"...\"` regex match. Overpass uses POSIX extended regular\n * expressions; we escape the ERE metacharacters plus the enclosing\n * double-quote and backslash so callers can pass arbitrary user-entered\n * keywords without worrying about regex injection or accidental pattern\n * matching.\n */\nfunction escapeOverpassRegex(value: string): string {\n return value.replace(/[\"\\\\.*+?^${}()|[\\]]/g, '\\\\$&');\n}\n\n/**\n * Tag keys Overpass treats as POI-like. Used when the caller doesn't\n * specify `types` — broad enough to cover businesses, landmarks, and\n * amenities without pulling back every single residential address.\n */\nconst POI_TAG_KEYS = [\n 'amenity',\n 'shop',\n 'tourism',\n 'leisure',\n 'office',\n 'historic',\n 'craft',\n];\n\n/**\n * OpenStreetMap provider using Nominatim API with in-memory caching\n */\nexport class OpenStreetMapProvider implements GeoProvider {\n private baseUrl = 'https://nominatim.openstreetmap.org';\n /**\n * Overpass endpoint for POI search. The public instance has a community\n * use-policy similar to Nominatim's — be polite, cache aggressively, and\n * consider a self-hosted instance if you'd hit it hard.\n */\n private overpassUrl = 'https://overpass-api.de/api/interpreter';\n private userAgent: string;\n private rateLimitDelay: number;\n private lastRequestTime = 0;\n private timeout: number;\n private maxResults: number;\n private cache: CacheAdapter | null = null;\n\n constructor(options: OpenStreetMapOptions) {\n this.userAgent = options.userAgent || '@happyvertical/geo (Node.js)';\n this.rateLimitDelay = options.rateLimitDelay || 1000; // 1 second default\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:osm',\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 * Enforce rate limiting by waiting if necessary\n */\n private async enforceRateLimit(): Promise<void> {\n const now = Date.now();\n const timeSinceLastRequest = now - this.lastRequestTime;\n\n if (timeSinceLastRequest < this.rateLimitDelay) {\n const waitTime = this.rateLimitDelay - timeSinceLastRequest;\n await new Promise((resolve) => setTimeout(resolve, waitTime));\n }\n\n this.lastRequestTime = Date.now();\n }\n\n /**\n * Make HTTP request to Nominatim API\n */\n private async fetchNominatim(\n endpoint: string,\n params: Record<string, string>,\n ): Promise<NominatimResult[]> {\n await this.enforceRateLimit();\n\n const queryParams = new URLSearchParams({\n ...params,\n format: 'json',\n addressdetails: '1',\n limit: this.maxResults.toString(),\n });\n\n const url = `${this.baseUrl}/${endpoint}?${queryParams.toString()}`;\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n headers: {\n 'User-Agent': this.userAgent,\n Accept: 'application/json',\n },\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (response.status === 429) {\n throw new RateLimitError('openstreetmap');\n }\n\n if (!response.ok) {\n throw new GeoError(\n `Nominatim API error: ${response.status} ${response.statusText}`,\n 'API_ERROR',\n 'openstreetmap',\n );\n }\n\n const data = await response.json();\n // Nominatim search returns array, reverse returns single object\n if (Array.isArray(data)) {\n return data;\n }\n return data ? [data as NominatimResult] : [];\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof GeoError) {\n throw error;\n }\n\n if ((error as Error).name === 'AbortError') {\n throw new GeoError('Request timeout', 'TIMEOUT', 'openstreetmap');\n }\n\n throw new GeoError(\n `Failed to fetch from Nominatim: ${(error as Error).message}`,\n 'FETCH_FAILED',\n 'openstreetmap',\n );\n }\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, 'openstreetmap');\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 results = await this.fetchNominatim('search', { q: query });\n const locations = results.map((result) =>\n this.mapNominatimResultToLocation(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 'openstreetmap',\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 'openstreetmap',\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 results = await this.fetchNominatim('reverse', {\n lat: latitude.toString(),\n lon: longitude.toString(),\n });\n\n // Nominatim reverse endpoint returns a single result or empty\n const locations = results.map((result) =>\n this.mapNominatimResultToLocation(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 'openstreetmap',\n );\n }\n }\n\n /**\n * Find POIs near a coordinate using the public Overpass API. Nominatim\n * only does address-level reverse geocoding — Overpass is the right tool\n * when you want \"every café within 200m\" kind of queries.\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 'openstreetmap',\n );\n }\n if (!(radiusMeters > 0)) {\n throw new InvalidQueryError(\n `radius ${radiusMeters}m must be > 0`,\n 'openstreetmap',\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 'openstreetmap',\n );\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 String(limit),\n );\n if (this.cache) {\n const cached = await this.cache.get<Location[]>(cacheKey);\n if (cached) return cached;\n }\n\n await this.enforceRateLimit();\n\n const query = this.buildOverpassQuery(\n latitude,\n longitude,\n radiusMeters,\n options,\n );\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(this.overpassUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'User-Agent': this.userAgent,\n Accept: 'application/json',\n },\n body: `data=${encodeURIComponent(query)}`,\n signal: controller.signal,\n });\n clearTimeout(timeoutId);\n\n if (response.status === 429) {\n throw new RateLimitError('openstreetmap');\n }\n if (!response.ok) {\n throw new GeoError(\n `Overpass API error: ${response.status} ${response.statusText}`,\n 'API_ERROR',\n 'openstreetmap',\n );\n }\n\n const data = (await response.json()) as { elements?: OverpassElement[] };\n const elements = Array.isArray(data.elements) ? data.elements : [];\n\n // Overpass returns each POI once per matched tag key, but when we\n // union multiple keys in the query elements can repeat — dedupe by\n // `(type, id)` so \"node/42\" only appears once regardless of how many\n // tag clauses matched.\n const seen = new Set<string>();\n const locations: Location[] = [];\n for (const element of elements) {\n const key = `${element.type}/${element.id}`;\n if (seen.has(key)) continue;\n seen.add(key);\n const location = this.mapOverpassElementToLocation(element);\n if (!location) continue;\n locations.push(location);\n if (locations.length >= limit) break;\n }\n\n if (this.cache) await this.cache.set(cacheKey, locations);\n return locations;\n } catch (error) {\n clearTimeout(timeoutId);\n if (error instanceof GeoError) throw error;\n if ((error as Error).name === 'AbortError') {\n throw new GeoError('Request timeout', 'TIMEOUT', 'openstreetmap');\n }\n throw new GeoError(\n `Failed to find POIs: ${(error as Error).message}`,\n 'POI_SEARCH_FAILED',\n 'openstreetmap',\n );\n }\n }\n\n /**\n * Build an Overpass QL query that collects nodes + ways with POI-shaped\n * tags within `radiusMeters` of the center point. `out center tags`\n * coerces ways/relations into point geometries so downstream mapping\n * doesn't have to deal with geometry types.\n */\n private buildOverpassQuery(\n latitude: number,\n longitude: number,\n radiusMeters: number,\n options: PoiSearchOptions,\n ): string {\n const around = `around:${radiusMeters},${latitude},${longitude}`;\n // `keyword` is documented as a free-text substring filter, but Overpass\n // interprets the right-hand side of `name~\"...\"` as a POSIX ERE. Escape\n // regex metacharacters so inputs like `C++`, `A.*`, or `Joes [Bar]`\n // match literally instead of silently changing the regex semantics or\n // producing an invalid query.\n const keywordFilter = options.keyword\n ? `[name~\"${escapeOverpassRegex(options.keyword)}\",i]`\n : '';\n const clauses: string[] = [];\n\n if (options.types && options.types.length > 0) {\n // Types filter: try each value against each POI tag key. We don't know\n // which key the caller intends (e.g. 'cafe' is an amenity; 'bakery'\n // could be either amenity or shop) so we fan out conservatively.\n for (const value of options.types) {\n const safe = value.replace(/\"/g, '\\\\\"');\n for (const key of POI_TAG_KEYS) {\n clauses.push(\n ` node(${around})[\"${key}\"=\"${safe}\"]${keywordFilter};`,\n );\n clauses.push(` way(${around})[\"${key}\"=\"${safe}\"]${keywordFilter};`);\n }\n }\n } else {\n for (const key of POI_TAG_KEYS) {\n clauses.push(` node(${around})[\"${key}\"]${keywordFilter};`);\n clauses.push(` way(${around})[\"${key}\"]${keywordFilter};`);\n }\n }\n\n return `[out:json][timeout:25];\\n(\\n${clauses.join('\\n')}\\n);\\nout center tags;`;\n }\n\n /**\n * Map an Overpass element to a standardized Location. Returns null for\n * tagless elements or ways without a `center` (which can happen when the\n * server falls back to geometry-less output under load).\n */\n private mapOverpassElementToLocation(\n element: OverpassElement,\n ): Location | null {\n const lat = element.lat ?? element.center?.lat;\n const lon = element.lon ?? element.center?.lon;\n if (lat == null || lon == null) return null;\n\n const tags = element.tags ?? {};\n const name =\n tags.name ||\n tags['name:en'] ||\n tags.brand ||\n tags.operator ||\n this.derivePoiLabelFromTags(tags) ||\n 'Unnamed place';\n\n return {\n id: `osm-${element.type}-${element.id}`,\n type: 'point_of_interest',\n name,\n latitude: lat,\n longitude: lon,\n addressComponents: {\n streetNumber: tags['addr:housenumber'],\n streetName: tags['addr:street'],\n city: tags['addr:city'],\n region: tags['addr:state'] || tags['addr:province'],\n country: tags['addr:country'],\n postalCode: tags['addr:postcode'],\n },\n countryCode: normalizeCountryCode(tags['addr:country']),\n raw: element,\n };\n }\n\n /**\n * Produce a readable label from a tag-only element (e.g. a shop with no\n * `name`). Picks the first POI-shaped tag value as a fallback so the\n * caller still gets something meaningful to show operators.\n */\n private derivePoiLabelFromTags(tags: Record<string, string>): string | null {\n for (const key of POI_TAG_KEYS) {\n const value = tags[key];\n if (value) return value.replace(/_/g, ' ');\n }\n return null;\n }\n\n /**\n * Map Nominatim result to standardized Location\n */\n private mapNominatimResultToLocation(result: NominatimResult): Location {\n const address = result.address || {};\n\n // Build address components\n const addressComponents: Location['addressComponents'] = {};\n\n if (address.house_number) {\n addressComponents.streetNumber = address.house_number;\n }\n if (address.road) {\n addressComponents.streetName = address.road;\n }\n if (address.city || address.town || address.village) {\n addressComponents.city = address.city || address.town || address.village;\n }\n if (address.state) {\n addressComponents.region = address.state;\n }\n if (address.country) {\n addressComponents.country = address.country;\n }\n if (address.postcode) {\n addressComponents.postalCode = address.postcode;\n }\n\n // Determine location type\n const type = mapOSMPlaceType(result.type || '', result.addresstype);\n\n // Parse coordinates\n const latitude = parseFloat(result.lat);\n const longitude = parseFloat(result.lon);\n\n return {\n id: `osm-${result.place_id}`,\n type,\n name: result.display_name,\n latitude,\n longitude,\n addressComponents,\n countryCode: normalizeCountryCode(address.country_code),\n raw: result,\n };\n }\n}\n"],"mappings":"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"}