@happyvertical/geo 0.74.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT.md +33 -0
- package/LICENSE +7 -0
- package/README.md +194 -0
- package/dist/chunks/google-Ci3_ec7t.js +342 -0
- package/dist/chunks/google-Ci3_ec7t.js.map +1 -0
- package/dist/chunks/openstreetmap-DEPHzMUV.js +419 -0
- package/dist/chunks/openstreetmap-DEPHzMUV.js.map +1 -0
- package/dist/cli/claude-context.d.ts +3 -0
- package/dist/cli/claude-context.d.ts.map +1 -0
- package/dist/cli/claude-context.js +21 -0
- package/dist/cli/claude-context.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +290 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/google.d.ts +49 -0
- package/dist/providers/google.d.ts.map +1 -0
- package/dist/providers/openstreetmap.d.ts +74 -0
- package/dist/providers/openstreetmap.d.ts.map +1 -0
- package/dist/shared/types.d.ts +233 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/utils.d.ts +52 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/dist/static-maps.d.ts +214 -0
- package/dist/static-maps.d.ts.map +1 -0
- package/metadata.json +31 -0
- package/package.json +69 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { getCache } from "@happyvertical/cache";
|
|
2
|
+
import { RateLimitError, GeoError, InvalidQueryError, normalizeCountryCode, mapOSMPlaceType, validateCoordinates } from "../index.js";
|
|
3
|
+
function escapeOverpassRegex(value) {
|
|
4
|
+
return value.replace(/["\\.*+?^${}()|[\]]/g, "\\$&");
|
|
5
|
+
}
|
|
6
|
+
const POI_TAG_KEYS = [
|
|
7
|
+
"amenity",
|
|
8
|
+
"shop",
|
|
9
|
+
"tourism",
|
|
10
|
+
"leisure",
|
|
11
|
+
"office",
|
|
12
|
+
"historic",
|
|
13
|
+
"craft"
|
|
14
|
+
];
|
|
15
|
+
class OpenStreetMapProvider {
|
|
16
|
+
baseUrl = "https://nominatim.openstreetmap.org";
|
|
17
|
+
/**
|
|
18
|
+
* Overpass endpoint for POI search. The public instance has a community
|
|
19
|
+
* use-policy similar to Nominatim's — be polite, cache aggressively, and
|
|
20
|
+
* consider a self-hosted instance if you'd hit it hard.
|
|
21
|
+
*/
|
|
22
|
+
overpassUrl = "https://overpass-api.de/api/interpreter";
|
|
23
|
+
userAgent;
|
|
24
|
+
rateLimitDelay;
|
|
25
|
+
lastRequestTime = 0;
|
|
26
|
+
timeout;
|
|
27
|
+
maxResults;
|
|
28
|
+
cache = null;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.userAgent = options.userAgent || "@happyvertical/geo (Node.js)";
|
|
31
|
+
this.rateLimitDelay = options.rateLimitDelay || 1e3;
|
|
32
|
+
this.timeout = options.timeout || 1e4;
|
|
33
|
+
this.maxResults = options.maxResults || 10;
|
|
34
|
+
this.initCache();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Initializes the memory cache for geocoding results
|
|
38
|
+
*/
|
|
39
|
+
async initCache() {
|
|
40
|
+
try {
|
|
41
|
+
this.cache = await getCache({
|
|
42
|
+
provider: "memory",
|
|
43
|
+
namespace: "geo:osm",
|
|
44
|
+
defaultTTL: 86400,
|
|
45
|
+
// 24 hour cache for location data
|
|
46
|
+
maxSize: 20 * 1024 * 1024,
|
|
47
|
+
// 20MB
|
|
48
|
+
maxEntries: 5e3,
|
|
49
|
+
evictionPolicy: "lru"
|
|
50
|
+
});
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.warn("Failed to initialize geo cache:", error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Generates a cache key for geocoding requests
|
|
57
|
+
*/
|
|
58
|
+
getCacheKey(type, ...parts) {
|
|
59
|
+
return `${type}:${parts.join(":")}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Enforce rate limiting by waiting if necessary
|
|
63
|
+
*/
|
|
64
|
+
async enforceRateLimit() {
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
67
|
+
if (timeSinceLastRequest < this.rateLimitDelay) {
|
|
68
|
+
const waitTime = this.rateLimitDelay - timeSinceLastRequest;
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
70
|
+
}
|
|
71
|
+
this.lastRequestTime = Date.now();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Make HTTP request to Nominatim API
|
|
75
|
+
*/
|
|
76
|
+
async fetchNominatim(endpoint, params) {
|
|
77
|
+
await this.enforceRateLimit();
|
|
78
|
+
const queryParams = new URLSearchParams({
|
|
79
|
+
...params,
|
|
80
|
+
format: "json",
|
|
81
|
+
addressdetails: "1",
|
|
82
|
+
limit: this.maxResults.toString()
|
|
83
|
+
});
|
|
84
|
+
const url = `${this.baseUrl}/${endpoint}?${queryParams.toString()}`;
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
87
|
+
try {
|
|
88
|
+
const response = await fetch(url, {
|
|
89
|
+
headers: {
|
|
90
|
+
"User-Agent": this.userAgent,
|
|
91
|
+
Accept: "application/json"
|
|
92
|
+
},
|
|
93
|
+
signal: controller.signal
|
|
94
|
+
});
|
|
95
|
+
clearTimeout(timeoutId);
|
|
96
|
+
if (response.status === 429) {
|
|
97
|
+
throw new RateLimitError("openstreetmap");
|
|
98
|
+
}
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
throw new GeoError(
|
|
101
|
+
`Nominatim API error: ${response.status} ${response.statusText}`,
|
|
102
|
+
"API_ERROR",
|
|
103
|
+
"openstreetmap"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
if (Array.isArray(data)) {
|
|
108
|
+
return data;
|
|
109
|
+
}
|
|
110
|
+
return data ? [data] : [];
|
|
111
|
+
} catch (error) {
|
|
112
|
+
clearTimeout(timeoutId);
|
|
113
|
+
if (error instanceof GeoError) {
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
if (error.name === "AbortError") {
|
|
117
|
+
throw new GeoError("Request timeout", "TIMEOUT", "openstreetmap");
|
|
118
|
+
}
|
|
119
|
+
throw new GeoError(
|
|
120
|
+
`Failed to fetch from Nominatim: ${error.message}`,
|
|
121
|
+
"FETCH_FAILED",
|
|
122
|
+
"openstreetmap"
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Look up locations based on a query string
|
|
128
|
+
*/
|
|
129
|
+
async lookup(query) {
|
|
130
|
+
if (!query || query.trim().length === 0) {
|
|
131
|
+
throw new InvalidQueryError(query, "openstreetmap");
|
|
132
|
+
}
|
|
133
|
+
const cacheKey = this.getCacheKey("lookup", query, String(this.maxResults));
|
|
134
|
+
if (this.cache) {
|
|
135
|
+
const cached = await this.cache.get(cacheKey);
|
|
136
|
+
if (cached) {
|
|
137
|
+
return cached;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const results = await this.fetchNominatim("search", { q: query });
|
|
142
|
+
const locations = results.map(
|
|
143
|
+
(result) => this.mapNominatimResultToLocation(result)
|
|
144
|
+
);
|
|
145
|
+
if (this.cache) {
|
|
146
|
+
await this.cache.set(cacheKey, locations);
|
|
147
|
+
}
|
|
148
|
+
return locations;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error instanceof GeoError) {
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
throw new GeoError(
|
|
154
|
+
`Failed to lookup location: ${error.message}`,
|
|
155
|
+
"LOOKUP_FAILED",
|
|
156
|
+
"openstreetmap"
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Reverse geocode from coordinates to location
|
|
162
|
+
*/
|
|
163
|
+
async reverseGeocode(latitude, longitude) {
|
|
164
|
+
const validation = validateCoordinates(latitude, longitude);
|
|
165
|
+
if (!validation.valid) {
|
|
166
|
+
throw new InvalidQueryError(
|
|
167
|
+
`${latitude}, ${longitude}: ${validation.error}`,
|
|
168
|
+
"openstreetmap"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const cacheKey = this.getCacheKey(
|
|
172
|
+
"reverse",
|
|
173
|
+
String(latitude),
|
|
174
|
+
String(longitude),
|
|
175
|
+
String(this.maxResults)
|
|
176
|
+
);
|
|
177
|
+
if (this.cache) {
|
|
178
|
+
const cached = await this.cache.get(cacheKey);
|
|
179
|
+
if (cached) {
|
|
180
|
+
return cached;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const results = await this.fetchNominatim("reverse", {
|
|
185
|
+
lat: latitude.toString(),
|
|
186
|
+
lon: longitude.toString()
|
|
187
|
+
});
|
|
188
|
+
const locations = results.map(
|
|
189
|
+
(result) => this.mapNominatimResultToLocation(result)
|
|
190
|
+
);
|
|
191
|
+
if (this.cache) {
|
|
192
|
+
await this.cache.set(cacheKey, locations);
|
|
193
|
+
}
|
|
194
|
+
return locations;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (error instanceof GeoError) {
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
throw new GeoError(
|
|
200
|
+
`Failed to reverse geocode: ${error.message}`,
|
|
201
|
+
"REVERSE_GEOCODE_FAILED",
|
|
202
|
+
"openstreetmap"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Find POIs near a coordinate using the public Overpass API. Nominatim
|
|
208
|
+
* only does address-level reverse geocoding — Overpass is the right tool
|
|
209
|
+
* when you want "every café within 200m" kind of queries.
|
|
210
|
+
*/
|
|
211
|
+
async findPoisNear(latitude, longitude, radiusMeters, options = {}) {
|
|
212
|
+
const validation = validateCoordinates(latitude, longitude);
|
|
213
|
+
if (!validation.valid) {
|
|
214
|
+
throw new InvalidQueryError(
|
|
215
|
+
`${latitude}, ${longitude}: ${validation.error}`,
|
|
216
|
+
"openstreetmap"
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (!(radiusMeters > 0)) {
|
|
220
|
+
throw new InvalidQueryError(
|
|
221
|
+
`radius ${radiusMeters}m must be > 0`,
|
|
222
|
+
"openstreetmap"
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
const limit = options.limit ?? this.maxResults;
|
|
226
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
227
|
+
throw new InvalidQueryError(
|
|
228
|
+
`limit ${limit} must be a positive integer`,
|
|
229
|
+
"openstreetmap"
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const cacheKey = this.getCacheKey(
|
|
233
|
+
"pois",
|
|
234
|
+
String(latitude),
|
|
235
|
+
String(longitude),
|
|
236
|
+
String(radiusMeters),
|
|
237
|
+
(options.types ?? []).join(","),
|
|
238
|
+
options.keyword ?? "",
|
|
239
|
+
String(limit)
|
|
240
|
+
);
|
|
241
|
+
if (this.cache) {
|
|
242
|
+
const cached = await this.cache.get(cacheKey);
|
|
243
|
+
if (cached) return cached;
|
|
244
|
+
}
|
|
245
|
+
await this.enforceRateLimit();
|
|
246
|
+
const query = this.buildOverpassQuery(
|
|
247
|
+
latitude,
|
|
248
|
+
longitude,
|
|
249
|
+
radiusMeters,
|
|
250
|
+
options
|
|
251
|
+
);
|
|
252
|
+
const controller = new AbortController();
|
|
253
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
254
|
+
try {
|
|
255
|
+
const response = await fetch(this.overpassUrl, {
|
|
256
|
+
method: "POST",
|
|
257
|
+
headers: {
|
|
258
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
259
|
+
"User-Agent": this.userAgent,
|
|
260
|
+
Accept: "application/json"
|
|
261
|
+
},
|
|
262
|
+
body: `data=${encodeURIComponent(query)}`,
|
|
263
|
+
signal: controller.signal
|
|
264
|
+
});
|
|
265
|
+
clearTimeout(timeoutId);
|
|
266
|
+
if (response.status === 429) {
|
|
267
|
+
throw new RateLimitError("openstreetmap");
|
|
268
|
+
}
|
|
269
|
+
if (!response.ok) {
|
|
270
|
+
throw new GeoError(
|
|
271
|
+
`Overpass API error: ${response.status} ${response.statusText}`,
|
|
272
|
+
"API_ERROR",
|
|
273
|
+
"openstreetmap"
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const data = await response.json();
|
|
277
|
+
const elements = Array.isArray(data.elements) ? data.elements : [];
|
|
278
|
+
const seen = /* @__PURE__ */ new Set();
|
|
279
|
+
const locations = [];
|
|
280
|
+
for (const element of elements) {
|
|
281
|
+
const key = `${element.type}/${element.id}`;
|
|
282
|
+
if (seen.has(key)) continue;
|
|
283
|
+
seen.add(key);
|
|
284
|
+
const location = this.mapOverpassElementToLocation(element);
|
|
285
|
+
if (!location) continue;
|
|
286
|
+
locations.push(location);
|
|
287
|
+
if (locations.length >= limit) break;
|
|
288
|
+
}
|
|
289
|
+
if (this.cache) await this.cache.set(cacheKey, locations);
|
|
290
|
+
return locations;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
clearTimeout(timeoutId);
|
|
293
|
+
if (error instanceof GeoError) throw error;
|
|
294
|
+
if (error.name === "AbortError") {
|
|
295
|
+
throw new GeoError("Request timeout", "TIMEOUT", "openstreetmap");
|
|
296
|
+
}
|
|
297
|
+
throw new GeoError(
|
|
298
|
+
`Failed to find POIs: ${error.message}`,
|
|
299
|
+
"POI_SEARCH_FAILED",
|
|
300
|
+
"openstreetmap"
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Build an Overpass QL query that collects nodes + ways with POI-shaped
|
|
306
|
+
* tags within `radiusMeters` of the center point. `out center tags`
|
|
307
|
+
* coerces ways/relations into point geometries so downstream mapping
|
|
308
|
+
* doesn't have to deal with geometry types.
|
|
309
|
+
*/
|
|
310
|
+
buildOverpassQuery(latitude, longitude, radiusMeters, options) {
|
|
311
|
+
const around = `around:${radiusMeters},${latitude},${longitude}`;
|
|
312
|
+
const keywordFilter = options.keyword ? `[name~"${escapeOverpassRegex(options.keyword)}",i]` : "";
|
|
313
|
+
const clauses = [];
|
|
314
|
+
if (options.types && options.types.length > 0) {
|
|
315
|
+
for (const value of options.types) {
|
|
316
|
+
const safe = value.replace(/"/g, '\\"');
|
|
317
|
+
for (const key of POI_TAG_KEYS) {
|
|
318
|
+
clauses.push(
|
|
319
|
+
` node(${around})["${key}"="${safe}"]${keywordFilter};`
|
|
320
|
+
);
|
|
321
|
+
clauses.push(` way(${around})["${key}"="${safe}"]${keywordFilter};`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
for (const key of POI_TAG_KEYS) {
|
|
326
|
+
clauses.push(` node(${around})["${key}"]${keywordFilter};`);
|
|
327
|
+
clauses.push(` way(${around})["${key}"]${keywordFilter};`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return `[out:json][timeout:25];
|
|
331
|
+
(
|
|
332
|
+
${clauses.join("\n")}
|
|
333
|
+
);
|
|
334
|
+
out center tags;`;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Map an Overpass element to a standardized Location. Returns null for
|
|
338
|
+
* tagless elements or ways without a `center` (which can happen when the
|
|
339
|
+
* server falls back to geometry-less output under load).
|
|
340
|
+
*/
|
|
341
|
+
mapOverpassElementToLocation(element) {
|
|
342
|
+
const lat = element.lat ?? element.center?.lat;
|
|
343
|
+
const lon = element.lon ?? element.center?.lon;
|
|
344
|
+
if (lat == null || lon == null) return null;
|
|
345
|
+
const tags = element.tags ?? {};
|
|
346
|
+
const name = tags.name || tags["name:en"] || tags.brand || tags.operator || this.derivePoiLabelFromTags(tags) || "Unnamed place";
|
|
347
|
+
return {
|
|
348
|
+
id: `osm-${element.type}-${element.id}`,
|
|
349
|
+
type: "point_of_interest",
|
|
350
|
+
name,
|
|
351
|
+
latitude: lat,
|
|
352
|
+
longitude: lon,
|
|
353
|
+
addressComponents: {
|
|
354
|
+
streetNumber: tags["addr:housenumber"],
|
|
355
|
+
streetName: tags["addr:street"],
|
|
356
|
+
city: tags["addr:city"],
|
|
357
|
+
region: tags["addr:state"] || tags["addr:province"],
|
|
358
|
+
country: tags["addr:country"],
|
|
359
|
+
postalCode: tags["addr:postcode"]
|
|
360
|
+
},
|
|
361
|
+
countryCode: normalizeCountryCode(tags["addr:country"]),
|
|
362
|
+
raw: element
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Produce a readable label from a tag-only element (e.g. a shop with no
|
|
367
|
+
* `name`). Picks the first POI-shaped tag value as a fallback so the
|
|
368
|
+
* caller still gets something meaningful to show operators.
|
|
369
|
+
*/
|
|
370
|
+
derivePoiLabelFromTags(tags) {
|
|
371
|
+
for (const key of POI_TAG_KEYS) {
|
|
372
|
+
const value = tags[key];
|
|
373
|
+
if (value) return value.replace(/_/g, " ");
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Map Nominatim result to standardized Location
|
|
379
|
+
*/
|
|
380
|
+
mapNominatimResultToLocation(result) {
|
|
381
|
+
const address = result.address || {};
|
|
382
|
+
const addressComponents = {};
|
|
383
|
+
if (address.house_number) {
|
|
384
|
+
addressComponents.streetNumber = address.house_number;
|
|
385
|
+
}
|
|
386
|
+
if (address.road) {
|
|
387
|
+
addressComponents.streetName = address.road;
|
|
388
|
+
}
|
|
389
|
+
if (address.city || address.town || address.village) {
|
|
390
|
+
addressComponents.city = address.city || address.town || address.village;
|
|
391
|
+
}
|
|
392
|
+
if (address.state) {
|
|
393
|
+
addressComponents.region = address.state;
|
|
394
|
+
}
|
|
395
|
+
if (address.country) {
|
|
396
|
+
addressComponents.country = address.country;
|
|
397
|
+
}
|
|
398
|
+
if (address.postcode) {
|
|
399
|
+
addressComponents.postalCode = address.postcode;
|
|
400
|
+
}
|
|
401
|
+
const type = mapOSMPlaceType(result.type || "", result.addresstype);
|
|
402
|
+
const latitude = parseFloat(result.lat);
|
|
403
|
+
const longitude = parseFloat(result.lon);
|
|
404
|
+
return {
|
|
405
|
+
id: `osm-${result.place_id}`,
|
|
406
|
+
type,
|
|
407
|
+
name: result.display_name,
|
|
408
|
+
latitude,
|
|
409
|
+
longitude,
|
|
410
|
+
addressComponents,
|
|
411
|
+
countryCode: normalizeCountryCode(address.country_code),
|
|
412
|
+
raw: result
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
export {
|
|
417
|
+
OpenStreetMapProvider
|
|
418
|
+
};
|
|
419
|
+
//# sourceMappingURL=openstreetmap-DEPHzMUV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openstreetmap-DEPHzMUV.js","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"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-context.d.ts","sourceRoot":"","sources":["../../src/cli/claude-context.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, copyFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const Dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const pkgRoot = join(Dirname, "../..");
|
|
7
|
+
const targetDir = join(process.cwd(), ".claude");
|
|
8
|
+
if (!existsSync(targetDir)) {
|
|
9
|
+
mkdirSync(targetDir, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
const pkgName = "geo";
|
|
12
|
+
const agentMdSrc = existsSync(join(pkgRoot, "AGENT.md")) ? join(pkgRoot, "AGENT.md") : join(pkgRoot, "CLAUDE.md");
|
|
13
|
+
const metaSrc = existsSync(join(pkgRoot, "metadata.json")) ? join(pkgRoot, "metadata.json") : join(pkgRoot, ".claude-meta.json");
|
|
14
|
+
if (existsSync(agentMdSrc)) {
|
|
15
|
+
copyFileSync(agentMdSrc, join(targetDir, `have-${pkgName}.md`));
|
|
16
|
+
}
|
|
17
|
+
if (existsSync(metaSrc)) {
|
|
18
|
+
copyFileSync(metaSrc, join(targetDir, `have-${pkgName}.meta.json`));
|
|
19
|
+
}
|
|
20
|
+
console.log(`✓ Installed @happyvertical/${pkgName} context to .claude/`);
|
|
21
|
+
//# sourceMappingURL=claude-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-context.js","sources":["../../src/cli/claude-context.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * CLI script to install agent context for @happyvertical/geo\n * Run the published context installer binary for this package.\n */\nimport { copyFileSync, existsSync, mkdirSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst Dirname = dirname(fileURLToPath(import.meta.url));\nconst pkgRoot = join(Dirname, '../..');\nconst targetDir = join(process.cwd(), '.claude');\n\nif (!existsSync(targetDir)) {\n mkdirSync(targetDir, { recursive: true });\n}\n\nconst pkgName = 'geo';\nconst agentMdSrc = existsSync(join(pkgRoot, 'AGENT.md'))\n ? join(pkgRoot, 'AGENT.md')\n : join(pkgRoot, 'CLAUDE.md');\nconst metaSrc = existsSync(join(pkgRoot, 'metadata.json'))\n ? join(pkgRoot, 'metadata.json')\n : join(pkgRoot, '.claude-meta.json');\n\nif (existsSync(agentMdSrc)) {\n copyFileSync(agentMdSrc, join(targetDir, `have-${pkgName}.md`));\n}\n\nif (existsSync(metaSrc)) {\n copyFileSync(metaSrc, join(targetDir, `have-${pkgName}.meta.json`));\n}\n\nconsole.log(`✓ Installed @happyvertical/${pkgName} context to .claude/`);\n"],"names":[],"mappings":";;;;AASA,MAAM,UAAU,QAAQ,cAAc,YAAY,GAAG,CAAC;AACtD,MAAM,UAAU,KAAK,SAAS,OAAO;AACrC,MAAM,YAAY,KAAK,QAAQ,IAAA,GAAO,SAAS;AAE/C,IAAI,CAAC,WAAW,SAAS,GAAG;AAC1B,YAAU,WAAW,EAAE,WAAW,KAAA,CAAM;AAC1C;AAEA,MAAM,UAAU;AAChB,MAAM,aAAa,WAAW,KAAK,SAAS,UAAU,CAAC,IACnD,KAAK,SAAS,UAAU,IACxB,KAAK,SAAS,WAAW;AAC7B,MAAM,UAAU,WAAW,KAAK,SAAS,eAAe,CAAC,IACrD,KAAK,SAAS,eAAe,IAC7B,KAAK,SAAS,mBAAmB;AAErC,IAAI,WAAW,UAAU,GAAG;AAC1B,eAAa,YAAY,KAAK,WAAW,QAAQ,OAAO,KAAK,CAAC;AAChE;AAEA,IAAI,WAAW,OAAO,GAAG;AACvB,eAAa,SAAS,KAAK,WAAW,QAAQ,OAAO,YAAY,CAAC;AACpE;AAEA,QAAQ,IAAI,8BAA8B,OAAO,sBAAsB;"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { GeoAdapter, GeoAdapterOptions } from './shared/types';
|
|
2
|
+
export * from './shared/types';
|
|
3
|
+
export * from './shared/utils';
|
|
4
|
+
export { fetchStaticMap, type GoogleMapType, getOGMapUrl, getStaticMapUrl, type MapboxStyle, type StaticMapMarker, type StaticMapOptions, type StaticMapProvider, type StaticMapResult, } from './static-maps';
|
|
5
|
+
/**
|
|
6
|
+
* Factory function to create a geo adapter instance
|
|
7
|
+
*
|
|
8
|
+
* Supports configuration via environment variables using the pattern:
|
|
9
|
+
* - HAVE_GEO_PROVIDER → provider ('google' | 'openstreetmap')
|
|
10
|
+
* - GOOGLE_MAPS_API_KEY → apiKey (for Google Maps provider)
|
|
11
|
+
*
|
|
12
|
+
* User-provided options always take precedence over environment variables.
|
|
13
|
+
*
|
|
14
|
+
* @param options - Configuration options for the geo provider (optional)
|
|
15
|
+
* @returns Promise resolving to a geo adapter that implements GeoAdapter
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* // Create Google Maps adapter with explicit options
|
|
20
|
+
* const googleGeo = await getGeoAdapter({
|
|
21
|
+
* provider: 'google',
|
|
22
|
+
* apiKey: process.env.GOOGLE_MAPS_API_KEY!
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Create adapter using environment variables
|
|
26
|
+
* // HAVE_GEO_PROVIDER=google
|
|
27
|
+
* // GOOGLE_MAPS_API_KEY=your-api-key
|
|
28
|
+
* const geoFromEnv = await getGeoAdapter();
|
|
29
|
+
*
|
|
30
|
+
* // Create OpenStreetMap adapter
|
|
31
|
+
* const osmGeo = await getGeoAdapter({
|
|
32
|
+
* provider: 'openstreetmap'
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* // Use the adapter
|
|
36
|
+
* const locations = await googleGeo.lookup('Eiffel Tower');
|
|
37
|
+
* const coords = await osmGeo.reverseGeocode(48.8584, 2.2945);
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function getGeoAdapter(options?: Partial<GeoAdapterOptions>): Promise<GeoAdapter>;
|
|
41
|
+
/** @internal */
|
|
42
|
+
export declare const PACKAGE_VERSION_INITIALIZED = true;
|
|
43
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|