@happyvertical/geo 0.80.0 → 0.80.1

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.
@@ -0,0 +1,244 @@
1
+ import { AuthenticationError, GeoError, InvalidQueryError, RateLimitError, mapGooglePlaceType, normalizeCountryCode, validateCoordinates } from "../index.js";
2
+ import { Client } from "@googlemaps/google-maps-services-js";
3
+ import { getCache } from "@happyvertical/cache";
4
+ //#region src/providers/google.ts
5
+ /**
6
+ * Google Maps provider implementation
7
+ */
8
+ /**
9
+ * Google Maps provider implementation with in-memory caching
10
+ */
11
+ var GoogleMapsProvider = class {
12
+ client;
13
+ apiKey;
14
+ timeout;
15
+ maxResults;
16
+ cache = null;
17
+ constructor(options) {
18
+ this.client = new Client({});
19
+ this.apiKey = options.apiKey;
20
+ this.timeout = options.timeout || 1e4;
21
+ this.maxResults = options.maxResults || 10;
22
+ this.initCache();
23
+ }
24
+ /**
25
+ * Initializes the memory cache for geocoding results
26
+ */
27
+ async initCache() {
28
+ try {
29
+ this.cache = await getCache({
30
+ provider: "memory",
31
+ namespace: "geo:google",
32
+ defaultTTL: 86400,
33
+ maxSize: 20 * 1024 * 1024,
34
+ maxEntries: 5e3,
35
+ evictionPolicy: "lru"
36
+ });
37
+ } catch (error) {
38
+ console.warn("Failed to initialize geo cache:", error);
39
+ }
40
+ }
41
+ /**
42
+ * Generates a cache key for geocoding requests
43
+ */
44
+ getCacheKey(type, ...parts) {
45
+ return `${type}:${parts.join(":")}`;
46
+ }
47
+ /**
48
+ * Look up locations based on a query string
49
+ */
50
+ async lookup(query) {
51
+ if (!query || query.trim().length === 0) throw new InvalidQueryError(query, "google");
52
+ const cacheKey = this.getCacheKey("lookup", query, String(this.maxResults));
53
+ if (this.cache) {
54
+ const cached = await this.cache.get(cacheKey);
55
+ if (cached) return cached;
56
+ }
57
+ try {
58
+ const response = await this.client.geocode({
59
+ params: {
60
+ address: query,
61
+ key: this.apiKey
62
+ },
63
+ timeout: this.timeout
64
+ });
65
+ if (response.data.status === "ZERO_RESULTS") return [];
66
+ if (response.data.status === "REQUEST_DENIED") throw new AuthenticationError("google");
67
+ if (response.data.status === "OVER_QUERY_LIMIT") throw new RateLimitError("google");
68
+ 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));
70
+ if (this.cache) await this.cache.set(cacheKey, locations);
71
+ return locations;
72
+ } catch (error) {
73
+ if (error instanceof GeoError) throw error;
74
+ throw new GeoError(`Failed to lookup location: ${error.message}`, "LOOKUP_FAILED", "google");
75
+ }
76
+ }
77
+ /**
78
+ * Reverse geocode from coordinates to location
79
+ */
80
+ async reverseGeocode(latitude, longitude) {
81
+ const validation = validateCoordinates(latitude, longitude);
82
+ if (!validation.valid) throw new InvalidQueryError(`${latitude}, ${longitude}: ${validation.error}`, "google");
83
+ const cacheKey = this.getCacheKey("reverse", String(latitude), String(longitude), String(this.maxResults));
84
+ if (this.cache) {
85
+ const cached = await this.cache.get(cacheKey);
86
+ if (cached) return cached;
87
+ }
88
+ try {
89
+ const response = await this.client.reverseGeocode({
90
+ params: {
91
+ latlng: {
92
+ lat: latitude,
93
+ lng: longitude
94
+ },
95
+ key: this.apiKey
96
+ },
97
+ timeout: this.timeout
98
+ });
99
+ if (response.data.status === "ZERO_RESULTS") return [];
100
+ if (response.data.status === "REQUEST_DENIED") throw new AuthenticationError("google");
101
+ if (response.data.status === "OVER_QUERY_LIMIT") throw new RateLimitError("google");
102
+ 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));
104
+ if (this.cache) await this.cache.set(cacheKey, locations);
105
+ return locations;
106
+ } catch (error) {
107
+ if (error instanceof GeoError) throw error;
108
+ throw new GeoError(`Failed to reverse geocode: ${error.message}`, "REVERSE_GEOCODE_FAILED", "google");
109
+ }
110
+ }
111
+ /**
112
+ * Find POIs near a coordinate using the Places API Nearby Search endpoint.
113
+ *
114
+ * Maps to `placesNearby` on the Google Maps Services SDK. Note that Places
115
+ * Nearby Search is a separate product line from Geocoding: it requires the
116
+ * Places API to be enabled for the supplied API key and is billed per
117
+ * request. Results are deduped across multi-type requests by `place_id`.
118
+ */
119
+ async findPoisNear(latitude, longitude, radiusMeters, options = {}) {
120
+ const validation = validateCoordinates(latitude, longitude);
121
+ if (!validation.valid) throw new InvalidQueryError(`${latitude}, ${longitude}: ${validation.error}`, "google");
122
+ if (!(radiusMeters > 0) || radiusMeters > 5e4) throw new InvalidQueryError(`radius ${radiusMeters}m must be in (0, 50000]`, "google");
123
+ const limit = options.limit ?? this.maxResults;
124
+ if (!Number.isInteger(limit) || limit < 1) throw new InvalidQueryError(`limit ${limit} must be a positive integer`, "google");
125
+ const types = options.types && options.types.length > 0 ? options.types : [void 0];
126
+ const cacheKey = this.getCacheKey("pois", String(latitude), String(longitude), String(radiusMeters), (options.types ?? []).join(","), options.keyword ?? "", options.language ?? "", String(limit));
127
+ if (this.cache) {
128
+ const cached = await this.cache.get(cacheKey);
129
+ if (cached) return cached;
130
+ }
131
+ try {
132
+ const merged = /* @__PURE__ */ new Map();
133
+ for (const type of types) {
134
+ let pageToken;
135
+ let pagesFetched = 0;
136
+ const maxPagesPerType = 3;
137
+ while (pagesFetched < maxPagesPerType) {
138
+ const params = pageToken ? {
139
+ pagetoken: pageToken,
140
+ key: this.apiKey
141
+ } : {
142
+ location: {
143
+ lat: latitude,
144
+ lng: longitude
145
+ },
146
+ radius: radiusMeters,
147
+ ...type ? { type } : {},
148
+ ...options.keyword ? { keyword: options.keyword } : {},
149
+ ...options.language ? { language: options.language } : {},
150
+ key: this.apiKey
151
+ };
152
+ const response = await this.client.placesNearby({
153
+ params,
154
+ timeout: this.timeout
155
+ });
156
+ if (response.data.status === "ZERO_RESULTS") break;
157
+ if (response.data.status === "REQUEST_DENIED") throw new AuthenticationError("google");
158
+ if (response.data.status === "OVER_QUERY_LIMIT") throw new RateLimitError("google");
159
+ if (response.data.status === "INVALID_REQUEST" && pageToken) {
160
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
161
+ continue;
162
+ }
163
+ if (response.data.status !== "OK") throw new GeoError(`Google Places API error: ${response.data.status}`, "API_ERROR", "google");
164
+ for (const result of response.data.results) {
165
+ const location = this.mapGooglePoiToLocation(result);
166
+ if (!merged.has(location.id)) merged.set(location.id, location);
167
+ if (merged.size >= limit) break;
168
+ }
169
+ pagesFetched += 1;
170
+ if (merged.size >= limit) break;
171
+ pageToken = response.data.next_page_token;
172
+ if (!pageToken) break;
173
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
174
+ }
175
+ if (merged.size >= limit) break;
176
+ }
177
+ const locations = [...merged.values()];
178
+ if (this.cache) await this.cache.set(cacheKey, locations);
179
+ return locations;
180
+ } catch (error) {
181
+ if (error instanceof GeoError) throw error;
182
+ throw new GeoError(`Failed to find POIs: ${error.message}`, "POI_SEARCH_FAILED", "google");
183
+ }
184
+ }
185
+ /**
186
+ * Map Google Places Nearby Search result to standardized Location. The
187
+ * Places schema is a superset of Geocoding (adds `name`, `vicinity`,
188
+ * `types[]` semantics slightly different from geocoding `types`) so this
189
+ * is a separate mapper from `mapGoogleResultToLocation`.
190
+ */
191
+ mapGooglePoiToLocation(result) {
192
+ const loc = result.geometry?.location;
193
+ const latitude = loc?.lat ?? 0;
194
+ const longitude = loc?.lng ?? 0;
195
+ const name = result.name ? result.vicinity ? `${result.name}, ${result.vicinity}` : result.name : result.vicinity || "Unknown place";
196
+ return {
197
+ id: result.place_id,
198
+ type: "point_of_interest",
199
+ name,
200
+ latitude,
201
+ longitude,
202
+ addressComponents: {},
203
+ countryCode: "XX",
204
+ raw: result
205
+ };
206
+ }
207
+ /**
208
+ * Map Google Geocoding API result to standardized Location
209
+ */
210
+ mapGoogleResultToLocation(result) {
211
+ const addressComponents = {};
212
+ let countryCode = "XX";
213
+ for (const component of result.address_components || []) {
214
+ const types = component.types;
215
+ if (types.includes("street_number")) addressComponents.streetNumber = component.long_name;
216
+ if (types.includes("route")) addressComponents.streetName = component.long_name;
217
+ if (types.includes("locality")) addressComponents.city = component.long_name;
218
+ if (types.includes("administrative_area_level_1")) addressComponents.region = component.long_name;
219
+ if (types.includes("country")) {
220
+ addressComponents.country = component.long_name;
221
+ countryCode = component.short_name;
222
+ }
223
+ if (types.includes("postal_code")) addressComponents.postalCode = component.long_name;
224
+ }
225
+ const location = result.geometry?.location;
226
+ const latitude = location?.lat ?? 0;
227
+ const longitude = location?.lng ?? 0;
228
+ const type = mapGooglePlaceType(result.types || []);
229
+ return {
230
+ id: result.place_id,
231
+ type,
232
+ name: result.formatted_address,
233
+ latitude,
234
+ longitude,
235
+ addressComponents,
236
+ countryCode: normalizeCountryCode(countryCode),
237
+ raw: result
238
+ };
239
+ }
240
+ };
241
+ //#endregion
242
+ export { GoogleMapsProvider };
243
+
244
+ //# sourceMappingURL=google-CZUa1ho0.js.map
@@ -0,0 +1 @@
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":"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"}
@@ -0,0 +1,314 @@
1
+ import { GeoError, InvalidQueryError, RateLimitError, mapOSMPlaceType, normalizeCountryCode, validateCoordinates } from "../index.js";
2
+ import { getCache } from "@happyvertical/cache";
3
+ //#region src/providers/openstreetmap.ts
4
+ /**
5
+ * Escape a literal string for safe interpolation into an Overpass
6
+ * `name~"..."` regex match. Overpass uses POSIX extended regular
7
+ * expressions; we escape the ERE metacharacters plus the enclosing
8
+ * double-quote and backslash so callers can pass arbitrary user-entered
9
+ * keywords without worrying about regex injection or accidental pattern
10
+ * matching.
11
+ */
12
+ function escapeOverpassRegex(value) {
13
+ return value.replace(/["\\.*+?^${}()|[\]]/g, "\\$&");
14
+ }
15
+ /**
16
+ * Tag keys Overpass treats as POI-like. Used when the caller doesn't
17
+ * specify `types` — broad enough to cover businesses, landmarks, and
18
+ * amenities without pulling back every single residential address.
19
+ */
20
+ var POI_TAG_KEYS = [
21
+ "amenity",
22
+ "shop",
23
+ "tourism",
24
+ "leisure",
25
+ "office",
26
+ "historic",
27
+ "craft"
28
+ ];
29
+ /**
30
+ * OpenStreetMap provider using Nominatim API with in-memory caching
31
+ */
32
+ var OpenStreetMapProvider = class {
33
+ baseUrl = "https://nominatim.openstreetmap.org";
34
+ /**
35
+ * Overpass endpoint for POI search. The public instance has a community
36
+ * use-policy similar to Nominatim's — be polite, cache aggressively, and
37
+ * consider a self-hosted instance if you'd hit it hard.
38
+ */
39
+ overpassUrl = "https://overpass-api.de/api/interpreter";
40
+ userAgent;
41
+ rateLimitDelay;
42
+ lastRequestTime = 0;
43
+ timeout;
44
+ maxResults;
45
+ cache = null;
46
+ constructor(options) {
47
+ this.userAgent = options.userAgent || "@happyvertical/geo (Node.js)";
48
+ this.rateLimitDelay = options.rateLimitDelay || 1e3;
49
+ this.timeout = options.timeout || 1e4;
50
+ this.maxResults = options.maxResults || 10;
51
+ this.initCache();
52
+ }
53
+ /**
54
+ * Initializes the memory cache for geocoding results
55
+ */
56
+ async initCache() {
57
+ try {
58
+ this.cache = await getCache({
59
+ provider: "memory",
60
+ namespace: "geo:osm",
61
+ defaultTTL: 86400,
62
+ maxSize: 20 * 1024 * 1024,
63
+ maxEntries: 5e3,
64
+ evictionPolicy: "lru"
65
+ });
66
+ } catch (error) {
67
+ console.warn("Failed to initialize geo cache:", error);
68
+ }
69
+ }
70
+ /**
71
+ * Generates a cache key for geocoding requests
72
+ */
73
+ getCacheKey(type, ...parts) {
74
+ return `${type}:${parts.join(":")}`;
75
+ }
76
+ /**
77
+ * Enforce rate limiting by waiting if necessary
78
+ */
79
+ async enforceRateLimit() {
80
+ const timeSinceLastRequest = Date.now() - this.lastRequestTime;
81
+ if (timeSinceLastRequest < this.rateLimitDelay) {
82
+ const waitTime = this.rateLimitDelay - timeSinceLastRequest;
83
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
84
+ }
85
+ this.lastRequestTime = Date.now();
86
+ }
87
+ /**
88
+ * Make HTTP request to Nominatim API
89
+ */
90
+ async fetchNominatim(endpoint, params) {
91
+ await this.enforceRateLimit();
92
+ const queryParams = new URLSearchParams({
93
+ ...params,
94
+ format: "json",
95
+ addressdetails: "1",
96
+ limit: this.maxResults.toString()
97
+ });
98
+ const url = `${this.baseUrl}/${endpoint}?${queryParams.toString()}`;
99
+ const controller = new AbortController();
100
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
101
+ try {
102
+ const response = await fetch(url, {
103
+ headers: {
104
+ "User-Agent": this.userAgent,
105
+ Accept: "application/json"
106
+ },
107
+ signal: controller.signal
108
+ });
109
+ clearTimeout(timeoutId);
110
+ if (response.status === 429) throw new RateLimitError("openstreetmap");
111
+ if (!response.ok) throw new GeoError(`Nominatim API error: ${response.status} ${response.statusText}`, "API_ERROR", "openstreetmap");
112
+ const data = await response.json();
113
+ if (Array.isArray(data)) return data;
114
+ return data ? [data] : [];
115
+ } catch (error) {
116
+ clearTimeout(timeoutId);
117
+ if (error instanceof GeoError) throw error;
118
+ if (error.name === "AbortError") throw new GeoError("Request timeout", "TIMEOUT", "openstreetmap");
119
+ throw new GeoError(`Failed to fetch from Nominatim: ${error.message}`, "FETCH_FAILED", "openstreetmap");
120
+ }
121
+ }
122
+ /**
123
+ * Look up locations based on a query string
124
+ */
125
+ async lookup(query) {
126
+ if (!query || query.trim().length === 0) throw new InvalidQueryError(query, "openstreetmap");
127
+ const cacheKey = this.getCacheKey("lookup", query, String(this.maxResults));
128
+ if (this.cache) {
129
+ const cached = await this.cache.get(cacheKey);
130
+ if (cached) return cached;
131
+ }
132
+ try {
133
+ const locations = (await this.fetchNominatim("search", { q: query })).map((result) => this.mapNominatimResultToLocation(result));
134
+ if (this.cache) await this.cache.set(cacheKey, locations);
135
+ return locations;
136
+ } catch (error) {
137
+ if (error instanceof GeoError) throw error;
138
+ throw new GeoError(`Failed to lookup location: ${error.message}`, "LOOKUP_FAILED", "openstreetmap");
139
+ }
140
+ }
141
+ /**
142
+ * Reverse geocode from coordinates to location
143
+ */
144
+ async reverseGeocode(latitude, longitude) {
145
+ const validation = validateCoordinates(latitude, longitude);
146
+ if (!validation.valid) throw new InvalidQueryError(`${latitude}, ${longitude}: ${validation.error}`, "openstreetmap");
147
+ const cacheKey = this.getCacheKey("reverse", String(latitude), String(longitude), String(this.maxResults));
148
+ if (this.cache) {
149
+ const cached = await this.cache.get(cacheKey);
150
+ if (cached) return cached;
151
+ }
152
+ try {
153
+ const locations = (await this.fetchNominatim("reverse", {
154
+ lat: latitude.toString(),
155
+ lon: longitude.toString()
156
+ })).map((result) => this.mapNominatimResultToLocation(result));
157
+ if (this.cache) await this.cache.set(cacheKey, locations);
158
+ return locations;
159
+ } catch (error) {
160
+ if (error instanceof GeoError) throw error;
161
+ throw new GeoError(`Failed to reverse geocode: ${error.message}`, "REVERSE_GEOCODE_FAILED", "openstreetmap");
162
+ }
163
+ }
164
+ /**
165
+ * Find POIs near a coordinate using the public Overpass API. Nominatim
166
+ * only does address-level reverse geocoding — Overpass is the right tool
167
+ * when you want "every café within 200m" kind of queries.
168
+ */
169
+ async findPoisNear(latitude, longitude, radiusMeters, options = {}) {
170
+ const validation = validateCoordinates(latitude, longitude);
171
+ if (!validation.valid) throw new InvalidQueryError(`${latitude}, ${longitude}: ${validation.error}`, "openstreetmap");
172
+ if (!(radiusMeters > 0)) throw new InvalidQueryError(`radius ${radiusMeters}m must be > 0`, "openstreetmap");
173
+ const limit = options.limit ?? this.maxResults;
174
+ if (!Number.isInteger(limit) || limit < 1) throw new InvalidQueryError(`limit ${limit} must be a positive integer`, "openstreetmap");
175
+ const cacheKey = this.getCacheKey("pois", String(latitude), String(longitude), String(radiusMeters), (options.types ?? []).join(","), options.keyword ?? "", String(limit));
176
+ if (this.cache) {
177
+ const cached = await this.cache.get(cacheKey);
178
+ if (cached) return cached;
179
+ }
180
+ await this.enforceRateLimit();
181
+ const query = this.buildOverpassQuery(latitude, longitude, radiusMeters, options);
182
+ const controller = new AbortController();
183
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
184
+ try {
185
+ const response = await fetch(this.overpassUrl, {
186
+ method: "POST",
187
+ headers: {
188
+ "Content-Type": "application/x-www-form-urlencoded",
189
+ "User-Agent": this.userAgent,
190
+ Accept: "application/json"
191
+ },
192
+ body: `data=${encodeURIComponent(query)}`,
193
+ signal: controller.signal
194
+ });
195
+ clearTimeout(timeoutId);
196
+ if (response.status === 429) throw new RateLimitError("openstreetmap");
197
+ if (!response.ok) throw new GeoError(`Overpass API error: ${response.status} ${response.statusText}`, "API_ERROR", "openstreetmap");
198
+ const data = await response.json();
199
+ const elements = Array.isArray(data.elements) ? data.elements : [];
200
+ const seen = /* @__PURE__ */ new Set();
201
+ const locations = [];
202
+ for (const element of elements) {
203
+ const key = `${element.type}/${element.id}`;
204
+ if (seen.has(key)) continue;
205
+ seen.add(key);
206
+ const location = this.mapOverpassElementToLocation(element);
207
+ if (!location) continue;
208
+ locations.push(location);
209
+ if (locations.length >= limit) break;
210
+ }
211
+ if (this.cache) await this.cache.set(cacheKey, locations);
212
+ return locations;
213
+ } catch (error) {
214
+ clearTimeout(timeoutId);
215
+ if (error instanceof GeoError) throw error;
216
+ if (error.name === "AbortError") throw new GeoError("Request timeout", "TIMEOUT", "openstreetmap");
217
+ throw new GeoError(`Failed to find POIs: ${error.message}`, "POI_SEARCH_FAILED", "openstreetmap");
218
+ }
219
+ }
220
+ /**
221
+ * Build an Overpass QL query that collects nodes + ways with POI-shaped
222
+ * tags within `radiusMeters` of the center point. `out center tags`
223
+ * coerces ways/relations into point geometries so downstream mapping
224
+ * doesn't have to deal with geometry types.
225
+ */
226
+ buildOverpassQuery(latitude, longitude, radiusMeters, options) {
227
+ const around = `around:${radiusMeters},${latitude},${longitude}`;
228
+ const keywordFilter = options.keyword ? `[name~"${escapeOverpassRegex(options.keyword)}",i]` : "";
229
+ const clauses = [];
230
+ if (options.types && options.types.length > 0) for (const value of options.types) {
231
+ const safe = value.replace(/"/g, "\\\"");
232
+ for (const key of POI_TAG_KEYS) {
233
+ clauses.push(` node(${around})["${key}"="${safe}"]${keywordFilter};`);
234
+ clauses.push(` way(${around})["${key}"="${safe}"]${keywordFilter};`);
235
+ }
236
+ }
237
+ else for (const key of POI_TAG_KEYS) {
238
+ clauses.push(` node(${around})["${key}"]${keywordFilter};`);
239
+ clauses.push(` way(${around})["${key}"]${keywordFilter};`);
240
+ }
241
+ return `[out:json][timeout:25];\n(\n${clauses.join("\n")}\n);\nout center tags;`;
242
+ }
243
+ /**
244
+ * Map an Overpass element to a standardized Location. Returns null for
245
+ * tagless elements or ways without a `center` (which can happen when the
246
+ * server falls back to geometry-less output under load).
247
+ */
248
+ mapOverpassElementToLocation(element) {
249
+ const lat = element.lat ?? element.center?.lat;
250
+ const lon = element.lon ?? element.center?.lon;
251
+ if (lat == null || lon == null) return null;
252
+ const tags = element.tags ?? {};
253
+ const name = tags.name || tags["name:en"] || tags.brand || tags.operator || this.derivePoiLabelFromTags(tags) || "Unnamed place";
254
+ return {
255
+ id: `osm-${element.type}-${element.id}`,
256
+ type: "point_of_interest",
257
+ name,
258
+ latitude: lat,
259
+ longitude: lon,
260
+ addressComponents: {
261
+ streetNumber: tags["addr:housenumber"],
262
+ streetName: tags["addr:street"],
263
+ city: tags["addr:city"],
264
+ region: tags["addr:state"] || tags["addr:province"],
265
+ country: tags["addr:country"],
266
+ postalCode: tags["addr:postcode"]
267
+ },
268
+ countryCode: normalizeCountryCode(tags["addr:country"]),
269
+ raw: element
270
+ };
271
+ }
272
+ /**
273
+ * Produce a readable label from a tag-only element (e.g. a shop with no
274
+ * `name`). Picks the first POI-shaped tag value as a fallback so the
275
+ * caller still gets something meaningful to show operators.
276
+ */
277
+ derivePoiLabelFromTags(tags) {
278
+ for (const key of POI_TAG_KEYS) {
279
+ const value = tags[key];
280
+ if (value) return value.replace(/_/g, " ");
281
+ }
282
+ return null;
283
+ }
284
+ /**
285
+ * Map Nominatim result to standardized Location
286
+ */
287
+ mapNominatimResultToLocation(result) {
288
+ const address = result.address || {};
289
+ const addressComponents = {};
290
+ if (address.house_number) addressComponents.streetNumber = address.house_number;
291
+ if (address.road) addressComponents.streetName = address.road;
292
+ if (address.city || address.town || address.village) addressComponents.city = address.city || address.town || address.village;
293
+ if (address.state) addressComponents.region = address.state;
294
+ if (address.country) addressComponents.country = address.country;
295
+ if (address.postcode) addressComponents.postalCode = address.postcode;
296
+ const type = mapOSMPlaceType(result.type || "", result.addresstype);
297
+ const latitude = parseFloat(result.lat);
298
+ const longitude = parseFloat(result.lon);
299
+ return {
300
+ id: `osm-${result.place_id}`,
301
+ type,
302
+ name: result.display_name,
303
+ latitude,
304
+ longitude,
305
+ addressComponents,
306
+ countryCode: normalizeCountryCode(address.country_code),
307
+ raw: result
308
+ };
309
+ }
310
+ };
311
+ //#endregion
312
+ export { OpenStreetMapProvider };
313
+
314
+ //# sourceMappingURL=openstreetmap-St4Kw-vS.js.map
@@ -0,0 +1 @@
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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"}