@cyanheads/openstreetmap-mcp-server 0.2.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/CLAUDE.md +393 -0
- package/Dockerfile +99 -0
- package/LICENSE +201 -0
- package/README.md +283 -0
- package/dist/config/server-config.d.ts +14 -0
- package/dist/config/server-config.d.ts.map +1 -0
- package/dist/config/server-config.js +47 -0
- package/dist/config/server-config.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-format.d.ts +18 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-format.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-format.js +36 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-format.js.map +1 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-geocode.tool.d.ts +59 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-geocode.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-geocode.tool.js +203 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-geocode.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-lookup.tool.d.ts +39 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-lookup.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-lookup.tool.js +136 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-lookup.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-query-bbox.tool.d.ts +57 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-query-bbox.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-query-bbox.tool.js +170 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-query-bbox.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-query-nearby.tool.d.ts +56 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-query-nearby.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-query-nearby.tool.js +174 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-query-nearby.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-query-raw.tool.d.ts +38 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-query-raw.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-query-raw.tool.js +124 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-query-raw.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-reverse.tool.d.ts +40 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-reverse.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-reverse.tool.js +129 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-reverse.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-tag-input.d.ts +21 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-tag-input.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-tag-input.js +22 -0
- package/dist/mcp-server/tools/definitions/openstreetmap-tag-input.js.map +1 -0
- package/dist/services/nominatim/nominatim-service.d.ts +24 -0
- package/dist/services/nominatim/nominatim-service.d.ts.map +1 -0
- package/dist/services/nominatim/nominatim-service.js +160 -0
- package/dist/services/nominatim/nominatim-service.js.map +1 -0
- package/dist/services/nominatim/types.d.ts +55 -0
- package/dist/services/nominatim/types.d.ts.map +1 -0
- package/dist/services/nominatim/types.js +6 -0
- package/dist/services/nominatim/types.js.map +1 -0
- package/dist/services/overpass/overpass-service.d.ts +25 -0
- package/dist/services/overpass/overpass-service.d.ts.map +1 -0
- package/dist/services/overpass/overpass-service.js +146 -0
- package/dist/services/overpass/overpass-service.js.map +1 -0
- package/dist/services/overpass/types.d.ts +59 -0
- package/dist/services/overpass/types.d.ts.map +1 -0
- package/dist/services/overpass/types.js +6 -0
- package/dist/services/overpass/types.js.map +1 -0
- package/package.json +91 -0
- package/server.json +153 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openstreetmap-reverse.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/openstreetmap-reverse.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAMjE,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgJ/B,CAAC"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Reverse geocoding tool — converts coordinates to nearest address or place.
|
|
3
|
+
* @module mcp-server/tools/definitions/openstreetmap-reverse.tool
|
|
4
|
+
*/
|
|
5
|
+
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
6
|
+
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
+
import { getNominatimService } from '../../../services/nominatim/nominatim-service.js';
|
|
8
|
+
import { appendPlaceLines } from './openstreetmap-format.js';
|
|
9
|
+
const ATTRIBUTION = 'Data © OpenStreetMap contributors, ODbL 1.0';
|
|
10
|
+
export const openstreetmapReverse = tool('openstreetmap_reverse', {
|
|
11
|
+
title: 'Reverse geocode coordinates to an address',
|
|
12
|
+
description: 'Convert latitude/longitude coordinates to the nearest address or place name via Nominatim/OpenStreetMap. ' +
|
|
13
|
+
'Returns the closest matching OSM object at the given coordinates. ' +
|
|
14
|
+
'Note: Nominatim finds the nearest indexed OSM object — in dense areas this may differ from the address at the exact coordinate. ' +
|
|
15
|
+
'Use zoom=18 for building-level accuracy, lower zoom values for coarser resolution (e.g., zoom=10 for city-level).',
|
|
16
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
17
|
+
input: z.object({
|
|
18
|
+
lat: z.number().min(-90).max(90).describe('Latitude in WGS84 decimal degrees.'),
|
|
19
|
+
lon: z.number().min(-180).max(180).describe('Longitude in WGS84 decimal degrees.'),
|
|
20
|
+
zoom: z
|
|
21
|
+
.number()
|
|
22
|
+
.int()
|
|
23
|
+
.min(3)
|
|
24
|
+
.max(18)
|
|
25
|
+
.default(18)
|
|
26
|
+
.describe('Address detail level, roughly corresponding to map zoom. 18=building, 16=street, 14=neighbourhood, 12=town, 10=city, 8=county, 5=state, 3=country.'),
|
|
27
|
+
layer: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('Restrict which OSM layer is matched. Comma-separated: address, poi, railway, natural, manmade. Default: address,poi.'),
|
|
31
|
+
extratags: z
|
|
32
|
+
.boolean()
|
|
33
|
+
.default(false)
|
|
34
|
+
.describe('Include extra OSM tags when available (phone, website, opening_hours, wikidata, etc.).'),
|
|
35
|
+
language: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('Preferred language for the result (BCP 47 code or Accept-Language string).'),
|
|
39
|
+
}),
|
|
40
|
+
output: z.object({
|
|
41
|
+
result: z
|
|
42
|
+
.object({
|
|
43
|
+
place_id: z.number().describe('Nominatim internal place ID.'),
|
|
44
|
+
osm_type: z.enum(['node', 'way', 'relation']).optional().describe('OSM object type.'),
|
|
45
|
+
osm_id: z
|
|
46
|
+
.number()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('OSM object ID. Combine with osm_type for openstreetmap_lookup.'),
|
|
49
|
+
lat: z.string().describe('Latitude of the matched OSM object.'),
|
|
50
|
+
lon: z.string().describe('Longitude of the matched OSM object.'),
|
|
51
|
+
display_name: z.string().describe('Full human-readable address.'),
|
|
52
|
+
name: z.string().optional().describe('Feature name if the result is a named place.'),
|
|
53
|
+
category: z
|
|
54
|
+
.string()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe('OSM feature category (e.g., "amenity", "building").'),
|
|
57
|
+
type: z.string().optional().describe('OSM feature type within category.'),
|
|
58
|
+
address: z
|
|
59
|
+
.record(z.string(), z.string())
|
|
60
|
+
.optional()
|
|
61
|
+
.describe('Structured address. Keys vary by feature type. Common: house_number, road, suburb, city, state, postcode, country, country_code.'),
|
|
62
|
+
boundingbox: z
|
|
63
|
+
.tuple([z.string(), z.string(), z.string(), z.string()])
|
|
64
|
+
.optional()
|
|
65
|
+
.describe('Bounding box as [south, north, west, east] strings.'),
|
|
66
|
+
extratags: z
|
|
67
|
+
.record(z.string(), z.string())
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('Additional OSM tags (phone, website, opening_hours, wikidata). Present only when extratags was requested.'),
|
|
70
|
+
})
|
|
71
|
+
.describe('The closest matching OSM object at the given coordinates.'),
|
|
72
|
+
attribution: z.string().describe('Required data attribution.'),
|
|
73
|
+
}),
|
|
74
|
+
errors: [
|
|
75
|
+
{
|
|
76
|
+
reason: 'no_coverage',
|
|
77
|
+
code: JsonRpcErrorCode.NotFound,
|
|
78
|
+
when: 'Nominatim returns an error indicating no OSM data at the given coordinates (e.g., open ocean or unmapped territory).',
|
|
79
|
+
recovery: 'Verify the coordinates are correct. Try a lower zoom value to match at a coarser level (e.g., zoom=10 for city-level).',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
async handler(input, ctx) {
|
|
83
|
+
const service = getNominatimService();
|
|
84
|
+
const raw = await service.reverse({
|
|
85
|
+
lat: input.lat,
|
|
86
|
+
lon: input.lon,
|
|
87
|
+
zoom: input.zoom,
|
|
88
|
+
extratags: input.extratags,
|
|
89
|
+
...(input.layer?.trim() ? { layer: input.layer } : {}),
|
|
90
|
+
...(input.language?.trim() ? { language: input.language } : {}),
|
|
91
|
+
}, ctx);
|
|
92
|
+
// Nominatim returns HTTP 200 with {"error": "Unable to geocode"} for unmapped areas
|
|
93
|
+
if (raw.error) {
|
|
94
|
+
throw ctx.fail('no_coverage', `No OSM data at coordinates (${input.lat}, ${input.lon}): ${raw.error}`, { ...ctx.recoveryFor('no_coverage') });
|
|
95
|
+
}
|
|
96
|
+
ctx.log.info('Reverse geocode result', { display_name: raw.display_name });
|
|
97
|
+
return {
|
|
98
|
+
result: {
|
|
99
|
+
place_id: raw.place_id,
|
|
100
|
+
...(raw.osm_type ? { osm_type: raw.osm_type } : {}),
|
|
101
|
+
...(raw.osm_id !== undefined ? { osm_id: raw.osm_id } : {}),
|
|
102
|
+
lat: raw.lat,
|
|
103
|
+
lon: raw.lon,
|
|
104
|
+
display_name: raw.display_name,
|
|
105
|
+
...(raw.name ? { name: raw.name } : {}),
|
|
106
|
+
...(raw.category ? { category: raw.category } : {}),
|
|
107
|
+
...(raw.type ? { type: raw.type } : {}),
|
|
108
|
+
...(raw.address ? { address: raw.address } : {}),
|
|
109
|
+
...(raw.boundingbox ? { boundingbox: raw.boundingbox } : {}),
|
|
110
|
+
...(raw.extratags ? { extratags: raw.extratags } : {}),
|
|
111
|
+
},
|
|
112
|
+
attribution: ATTRIBUTION,
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
format: (result) => {
|
|
116
|
+
const r = result.result;
|
|
117
|
+
const lines = [];
|
|
118
|
+
if (r.name)
|
|
119
|
+
lines.push(`## ${r.name}`);
|
|
120
|
+
lines.push(`**Address:** ${r.display_name}`);
|
|
121
|
+
lines.push(`**Coordinates:** ${r.lat}, ${r.lon}`);
|
|
122
|
+
lines.push(`**Place ID:** ${r.place_id}`);
|
|
123
|
+
appendPlaceLines(lines, r);
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push(`*${result.attribution}*`);
|
|
126
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
//# sourceMappingURL=openstreetmap-reverse.tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openstreetmap-reverse.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/openstreetmap-reverse.tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared tag input validation and resolution for Overpass convenience tools.
|
|
3
|
+
* @module mcp-server/tools/definitions/openstreetmap-tag-input
|
|
4
|
+
*/
|
|
5
|
+
/** Resolved tag key/value pair extracted from amenity shortcut or explicit tag_key/tag_value. */
|
|
6
|
+
export type ResolvedTag = {
|
|
7
|
+
tagKey: string;
|
|
8
|
+
tagValue: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Validate and resolve the mutually-exclusive amenity / tag_key+tag_value input pattern.
|
|
12
|
+
* Throws an error string on invalid input; callers translate it to ctx.fail.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveTagInput(input: {
|
|
15
|
+
amenity?: string | undefined;
|
|
16
|
+
tag_key?: string | undefined;
|
|
17
|
+
tag_value?: string | undefined;
|
|
18
|
+
}): ResolvedTag | {
|
|
19
|
+
error: 'both' | 'neither';
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=openstreetmap-tag-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openstreetmap-tag-input.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/openstreetmap-tag-input.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,iGAAiG;AACjG,MAAM,MAAM,WAAW,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/D;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC,GAAG,WAAW,GAAG;IAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAY9C"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared tag input validation and resolution for Overpass convenience tools.
|
|
3
|
+
* @module mcp-server/tools/definitions/openstreetmap-tag-input
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Validate and resolve the mutually-exclusive amenity / tag_key+tag_value input pattern.
|
|
7
|
+
* Throws an error string on invalid input; callers translate it to ctx.fail.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveTagInput(input) {
|
|
10
|
+
const hasAmenity = Boolean(input.amenity?.trim());
|
|
11
|
+
const hasTagKey = Boolean(input.tag_key?.trim());
|
|
12
|
+
const hasTagValue = Boolean(input.tag_value?.trim());
|
|
13
|
+
if (hasAmenity && (hasTagKey || hasTagValue))
|
|
14
|
+
return { error: 'both' };
|
|
15
|
+
if (!hasAmenity && !hasTagKey)
|
|
16
|
+
return { error: 'neither' };
|
|
17
|
+
return {
|
|
18
|
+
tagKey: hasAmenity ? 'amenity' : (input.tag_key ?? ''),
|
|
19
|
+
tagValue: hasAmenity ? (input.amenity ?? '') : (input.tag_value ?? ''),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=openstreetmap-tag-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openstreetmap-tag-input.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/openstreetmap-tag-input.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,KAI/B;IACC,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IACjD,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAErD,IAAI,UAAU,IAAI,CAAC,SAAS,IAAI,WAAW,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IACvE,IAAI,CAAC,UAAU,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAE3D,OAAO;QACL,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;QACtD,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,IAAI,EAAE,CAAC;KACvE,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Nominatim API client with rate limiting, retry, and session caching.
|
|
3
|
+
* @module services/nominatim/nominatim-service
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@cyanheads/mcp-ts-core';
|
|
6
|
+
import type { AppConfig } from '@cyanheads/mcp-ts-core/config';
|
|
7
|
+
import type { StorageService } from '@cyanheads/mcp-ts-core/storage';
|
|
8
|
+
import type { NominatimLookupParams, NominatimPlace, NominatimReverseParams, NominatimSearchParams } from './types.js';
|
|
9
|
+
export declare class NominatimService {
|
|
10
|
+
private lastRequestTime;
|
|
11
|
+
constructor(_config: AppConfig, _storage: StorageService);
|
|
12
|
+
/** Enforce the 1 req/sec rate limit. */
|
|
13
|
+
private throttle;
|
|
14
|
+
private userAgent;
|
|
15
|
+
private baseUrl;
|
|
16
|
+
private buildCacheKey;
|
|
17
|
+
private fetchJson;
|
|
18
|
+
search(params: NominatimSearchParams, ctx: Context): Promise<NominatimPlace[]>;
|
|
19
|
+
reverse(params: NominatimReverseParams, ctx: Context): Promise<NominatimPlace>;
|
|
20
|
+
lookup(params: NominatimLookupParams, ctx: Context): Promise<NominatimPlace[]>;
|
|
21
|
+
}
|
|
22
|
+
export declare function initNominatimService(config: AppConfig, storage: StorageService): void;
|
|
23
|
+
export declare function getNominatimService(): NominatimService;
|
|
24
|
+
//# sourceMappingURL=nominatim-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nominatim-service.d.ts","sourceRoot":"","sources":["../../../src/services/nominatim/nominatim-service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAE/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAIrE,OAAO,KAAK,EACV,qBAAqB,EACrB,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAQpB,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,eAAe,CAAK;gBAGhB,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,cAAc;IAExD,wCAAwC;YAC1B,QAAQ;IAStB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,aAAa;YAKP,SAAS;IAuCjB,MAAM,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IA0C9E,OAAO,CAAC,MAAM,EAAE,sBAAsB,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC;IAiC9E,MAAM,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;CA6BrF;AAMD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAErF;AAED,wBAAgB,mBAAmB,IAAI,gBAAgB,CAKtD"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Nominatim API client with rate limiting, retry, and session caching.
|
|
3
|
+
* @module services/nominatim/nominatim-service
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
+
import { fetchWithTimeout, withRetry } from '@cyanheads/mcp-ts-core/utils';
|
|
8
|
+
import { getServerConfig } from '../../config/server-config.js';
|
|
9
|
+
/** Cache TTL: 60 minutes (geocoding results rarely change within a session). */
|
|
10
|
+
const CACHE_TTL_SECONDS = 3600;
|
|
11
|
+
/** Nominatim enforces a strict 1 req/sec limit. */
|
|
12
|
+
const MIN_REQUEST_INTERVAL_MS = 1050;
|
|
13
|
+
export class NominatimService {
|
|
14
|
+
lastRequestTime = 0;
|
|
15
|
+
// config and storage reserved for future use (private instance auth, custom storage)
|
|
16
|
+
constructor(_config, _storage) { }
|
|
17
|
+
/** Enforce the 1 req/sec rate limit. */
|
|
18
|
+
async throttle() {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
const elapsed = now - this.lastRequestTime;
|
|
21
|
+
if (elapsed < MIN_REQUEST_INTERVAL_MS) {
|
|
22
|
+
await new Promise((resolve) => setTimeout(resolve, MIN_REQUEST_INTERVAL_MS - elapsed));
|
|
23
|
+
}
|
|
24
|
+
this.lastRequestTime = Date.now();
|
|
25
|
+
}
|
|
26
|
+
userAgent() {
|
|
27
|
+
return getServerConfig().nominatimUserAgent;
|
|
28
|
+
}
|
|
29
|
+
baseUrl() {
|
|
30
|
+
return getServerConfig().nominatimBaseUrl;
|
|
31
|
+
}
|
|
32
|
+
buildCacheKey(endpoint, params) {
|
|
33
|
+
const hash = createHash('sha256').update(JSON.stringify(params)).digest('hex').slice(0, 16);
|
|
34
|
+
return `nominatim/${endpoint}/${hash}`;
|
|
35
|
+
}
|
|
36
|
+
async fetchJson(path, params, ctx) {
|
|
37
|
+
const url = new URL(path, this.baseUrl());
|
|
38
|
+
url.searchParams.set('format', 'jsonv2');
|
|
39
|
+
url.searchParams.set('addressdetails', '1');
|
|
40
|
+
for (const [key, value] of Object.entries(params)) {
|
|
41
|
+
if (value !== undefined && value !== '') {
|
|
42
|
+
url.searchParams.set(key, value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
await this.throttle();
|
|
46
|
+
const response = await fetchWithTimeout(url.toString(), 30_000, ctx, {
|
|
47
|
+
headers: {
|
|
48
|
+
'User-Agent': this.userAgent(),
|
|
49
|
+
Accept: 'application/json',
|
|
50
|
+
},
|
|
51
|
+
signal: ctx.signal,
|
|
52
|
+
});
|
|
53
|
+
const text = await response.text();
|
|
54
|
+
if (/^\s*<(!DOCTYPE\s+html|html[\s>])/i.test(text)) {
|
|
55
|
+
throw serviceUnavailable('Nominatim returned an HTML error page — likely rate-limited or unavailable.');
|
|
56
|
+
}
|
|
57
|
+
return JSON.parse(text);
|
|
58
|
+
}
|
|
59
|
+
async search(params, ctx) {
|
|
60
|
+
const cacheKey = this.buildCacheKey('search', params);
|
|
61
|
+
const cached = await ctx.state.get(cacheKey);
|
|
62
|
+
if (cached != null) {
|
|
63
|
+
ctx.log.debug('Nominatim search cache hit', { cacheKey });
|
|
64
|
+
return cached;
|
|
65
|
+
}
|
|
66
|
+
const queryParams = {};
|
|
67
|
+
const setIfTruthy = (key, val) => {
|
|
68
|
+
if (val)
|
|
69
|
+
queryParams[key] = String(val);
|
|
70
|
+
};
|
|
71
|
+
setIfTruthy('q', params.q);
|
|
72
|
+
setIfTruthy('street', params.street);
|
|
73
|
+
setIfTruthy('city', params.city);
|
|
74
|
+
setIfTruthy('county', params.county);
|
|
75
|
+
setIfTruthy('state', params.state);
|
|
76
|
+
setIfTruthy('country', params.country);
|
|
77
|
+
setIfTruthy('postalcode', params.postalcode);
|
|
78
|
+
setIfTruthy('limit', params.limit);
|
|
79
|
+
setIfTruthy('countrycodes', params.countrycodes);
|
|
80
|
+
setIfTruthy('layer', params.layer);
|
|
81
|
+
setIfTruthy('featuretype', params.featureType);
|
|
82
|
+
if (params.extratags)
|
|
83
|
+
queryParams.extratags = '1';
|
|
84
|
+
setIfTruthy('accept_language', params.language);
|
|
85
|
+
ctx.log.info('Nominatim search', { params });
|
|
86
|
+
const results = await withRetry(() => this.fetchJson('/search', queryParams, ctx), {
|
|
87
|
+
operation: 'nominatim.search',
|
|
88
|
+
context: ctx,
|
|
89
|
+
baseDelayMs: 1100,
|
|
90
|
+
signal: ctx.signal,
|
|
91
|
+
});
|
|
92
|
+
await ctx.state.set(cacheKey, results, { ttl: CACHE_TTL_SECONDS });
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
async reverse(params, ctx) {
|
|
96
|
+
const cacheKey = this.buildCacheKey('reverse', params);
|
|
97
|
+
const cached = await ctx.state.get(cacheKey);
|
|
98
|
+
if (cached != null) {
|
|
99
|
+
ctx.log.debug('Nominatim reverse cache hit', { cacheKey });
|
|
100
|
+
return cached;
|
|
101
|
+
}
|
|
102
|
+
const queryParams = {
|
|
103
|
+
lat: String(params.lat),
|
|
104
|
+
lon: String(params.lon),
|
|
105
|
+
};
|
|
106
|
+
if (params.zoom !== undefined)
|
|
107
|
+
queryParams.zoom = String(params.zoom);
|
|
108
|
+
if (params.layer)
|
|
109
|
+
queryParams.layer = params.layer;
|
|
110
|
+
if (params.extratags)
|
|
111
|
+
queryParams.extratags = '1';
|
|
112
|
+
if (params.language)
|
|
113
|
+
queryParams.accept_language = params.language;
|
|
114
|
+
ctx.log.info('Nominatim reverse', { lat: params.lat, lon: params.lon });
|
|
115
|
+
const result = await withRetry(() => this.fetchJson('/reverse', queryParams, ctx), {
|
|
116
|
+
operation: 'nominatim.reverse',
|
|
117
|
+
context: ctx,
|
|
118
|
+
baseDelayMs: 1100,
|
|
119
|
+
signal: ctx.signal,
|
|
120
|
+
});
|
|
121
|
+
await ctx.state.set(cacheKey, result, { ttl: CACHE_TTL_SECONDS });
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
async lookup(params, ctx) {
|
|
125
|
+
const cacheKey = this.buildCacheKey('lookup', params);
|
|
126
|
+
const cached = await ctx.state.get(cacheKey);
|
|
127
|
+
if (cached != null) {
|
|
128
|
+
ctx.log.debug('Nominatim lookup cache hit', { cacheKey });
|
|
129
|
+
return cached;
|
|
130
|
+
}
|
|
131
|
+
const queryParams = {
|
|
132
|
+
osm_ids: params.osm_ids.join(','),
|
|
133
|
+
};
|
|
134
|
+
if (params.extratags)
|
|
135
|
+
queryParams.extratags = '1';
|
|
136
|
+
if (params.language)
|
|
137
|
+
queryParams.accept_language = params.language;
|
|
138
|
+
ctx.log.info('Nominatim lookup', { osm_ids: params.osm_ids });
|
|
139
|
+
const results = await withRetry(() => this.fetchJson('/lookup', queryParams, ctx), {
|
|
140
|
+
operation: 'nominatim.lookup',
|
|
141
|
+
context: ctx,
|
|
142
|
+
baseDelayMs: 1100,
|
|
143
|
+
signal: ctx.signal,
|
|
144
|
+
});
|
|
145
|
+
await ctx.state.set(cacheKey, results, { ttl: CACHE_TTL_SECONDS });
|
|
146
|
+
return results;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// --- Init/accessor pattern ---
|
|
150
|
+
let _service;
|
|
151
|
+
export function initNominatimService(config, storage) {
|
|
152
|
+
_service = new NominatimService(config, storage);
|
|
153
|
+
}
|
|
154
|
+
export function getNominatimService() {
|
|
155
|
+
if (!_service) {
|
|
156
|
+
throw new Error('NominatimService not initialized — call initNominatimService() in setup()');
|
|
157
|
+
}
|
|
158
|
+
return _service;
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=nominatim-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nominatim-service.js","sourceRoot":"","sources":["../../../src/services/nominatim/nominatim-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Types for the Nominatim API responses and domain models.
|
|
3
|
+
* @module services/nominatim/types
|
|
4
|
+
*/
|
|
5
|
+
/** Raw Nominatim place result (jsonv2 format). Fields vary by feature type. */
|
|
6
|
+
export type NominatimPlace = {
|
|
7
|
+
place_id: number;
|
|
8
|
+
osm_type?: 'node' | 'way' | 'relation';
|
|
9
|
+
osm_id?: number;
|
|
10
|
+
lat: string;
|
|
11
|
+
lon: string;
|
|
12
|
+
display_name: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
category?: string;
|
|
15
|
+
type?: string;
|
|
16
|
+
place_rank?: number;
|
|
17
|
+
importance?: number;
|
|
18
|
+
addresstype?: string;
|
|
19
|
+
address?: Record<string, string>;
|
|
20
|
+
boundingbox?: [string, string, string, string];
|
|
21
|
+
extratags?: Record<string, string>;
|
|
22
|
+
error?: string;
|
|
23
|
+
};
|
|
24
|
+
/** Parameters for the Nominatim /search endpoint. */
|
|
25
|
+
export type NominatimSearchParams = {
|
|
26
|
+
q?: string;
|
|
27
|
+
street?: string;
|
|
28
|
+
city?: string;
|
|
29
|
+
county?: string;
|
|
30
|
+
state?: string;
|
|
31
|
+
country?: string;
|
|
32
|
+
postalcode?: string;
|
|
33
|
+
limit?: number;
|
|
34
|
+
countrycodes?: string;
|
|
35
|
+
layer?: string;
|
|
36
|
+
featureType?: string;
|
|
37
|
+
extratags?: boolean;
|
|
38
|
+
language?: string;
|
|
39
|
+
};
|
|
40
|
+
/** Parameters for the Nominatim /reverse endpoint. */
|
|
41
|
+
export type NominatimReverseParams = {
|
|
42
|
+
lat: number;
|
|
43
|
+
lon: number;
|
|
44
|
+
zoom?: number;
|
|
45
|
+
layer?: string;
|
|
46
|
+
extratags?: boolean;
|
|
47
|
+
language?: string;
|
|
48
|
+
};
|
|
49
|
+
/** Parameters for the Nominatim /lookup endpoint. */
|
|
50
|
+
export type NominatimLookupParams = {
|
|
51
|
+
osm_ids: string[];
|
|
52
|
+
extratags?: boolean;
|
|
53
|
+
language?: string;
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/nominatim/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,+EAA+E;AAC/E,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,UAAU,CAAC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,qDAAqD;AACrD,MAAM,MAAM,qBAAqB,GAAG;IAClC,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,sDAAsD;AACtD,MAAM,MAAM,sBAAsB,GAAG;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,qDAAqD;AACrD,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/services/nominatim/types.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Overpass API client with retry, session caching, and QL query builders.
|
|
3
|
+
* @module services/overpass/overpass-service
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@cyanheads/mcp-ts-core';
|
|
6
|
+
import type { AppConfig } from '@cyanheads/mcp-ts-core/config';
|
|
7
|
+
import type { StorageService } from '@cyanheads/mcp-ts-core/storage';
|
|
8
|
+
import type { OverpassAroundParams, OverpassBboxParams, OverpassElement, OverpassPoi, OverpassResponse } from './types.js';
|
|
9
|
+
export declare class OverpassService {
|
|
10
|
+
constructor(_config: AppConfig, _storage: StorageService);
|
|
11
|
+
private endpoint;
|
|
12
|
+
private buildCacheKey;
|
|
13
|
+
/** Build an around-filter Overpass QL query. */
|
|
14
|
+
buildAroundQuery(params: OverpassAroundParams): string;
|
|
15
|
+
/** Build a bounding-box Overpass QL query. */
|
|
16
|
+
buildBboxQuery(params: OverpassBboxParams): string;
|
|
17
|
+
private executeQuery;
|
|
18
|
+
/** Execute a generated or raw Overpass QL query and return raw elements. */
|
|
19
|
+
query(ql: string, ctx: Context): Promise<OverpassResponse>;
|
|
20
|
+
/** Normalize Overpass elements into POI-friendly shape. */
|
|
21
|
+
normalizeElements(elements: OverpassElement[]): OverpassPoi[];
|
|
22
|
+
}
|
|
23
|
+
export declare function initOverpassService(config: AppConfig, storage: StorageService): void;
|
|
24
|
+
export declare function getOverpassService(): OverpassService;
|
|
25
|
+
//# sourceMappingURL=overpass-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overpass-service.d.ts","sourceRoot":"","sources":["../../../src/services/overpass/overpass-service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAE/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAIrE,OAAO,KAAK,EACV,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAWpB,qBAAa,eAAe;gBAEd,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,cAAc;IAExD,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,aAAa;IAKrB,gDAAgD;IAChD,gBAAgB,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM;IActD,8CAA8C;IAC9C,cAAc,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM;YAepC,YAAY;IA6E1B,4EAA4E;IAC5E,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAK1D,2DAA2D;IAC3D,iBAAiB,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,WAAW,EAAE;CAe9D;AAMD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAEpF;AAED,wBAAgB,kBAAkB,IAAI,eAAe,CAKpD"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Overpass API client with retry, session caching, and QL query builders.
|
|
3
|
+
* @module services/overpass/overpass-service
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { serviceUnavailable, timeout as timeoutError } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
+
import { withRetry } from '@cyanheads/mcp-ts-core/utils';
|
|
8
|
+
import { getServerConfig } from '../../config/server-config.js';
|
|
9
|
+
/** Cache TTL for Overpass results: 10 minutes (more volatile than geocoding). */
|
|
10
|
+
const CACHE_TTL_SECONDS = 600;
|
|
11
|
+
/** Overpass timeout error messages that indicate query-level timeout. */
|
|
12
|
+
const OVERPASS_TIMEOUT_PATTERN = /runtime error|query timed out|timed out/i;
|
|
13
|
+
/** Overpass out-of-memory error patterns. */
|
|
14
|
+
const OVERPASS_OOM_PATTERN = /out of memory|query run out/i;
|
|
15
|
+
export class OverpassService {
|
|
16
|
+
// config and storage reserved for future use (private instance auth, custom storage)
|
|
17
|
+
constructor(_config, _storage) { }
|
|
18
|
+
endpoint() {
|
|
19
|
+
return getServerConfig().overpassBaseUrl;
|
|
20
|
+
}
|
|
21
|
+
buildCacheKey(query) {
|
|
22
|
+
const hash = createHash('sha256').update(query).digest('hex').slice(0, 16);
|
|
23
|
+
return `overpass/${hash}`;
|
|
24
|
+
}
|
|
25
|
+
/** Build an around-filter Overpass QL query. */
|
|
26
|
+
buildAroundQuery(params) {
|
|
27
|
+
const { lat, lon, radiusMeters, tagKey, tagValue, elementTypes, timeoutSeconds } = params;
|
|
28
|
+
const filter = `(around:${radiusMeters},${lat},${lon})`;
|
|
29
|
+
const tagFilter = `["${tagKey}"="${tagValue}"]`;
|
|
30
|
+
const lines = [
|
|
31
|
+
`[out:json][timeout:${timeoutSeconds}];`,
|
|
32
|
+
'(',
|
|
33
|
+
...elementTypes.map((t) => ` ${t}${tagFilter}${filter};`),
|
|
34
|
+
');',
|
|
35
|
+
'out center tags;',
|
|
36
|
+
];
|
|
37
|
+
return lines.join('\n');
|
|
38
|
+
}
|
|
39
|
+
/** Build a bounding-box Overpass QL query. */
|
|
40
|
+
buildBboxQuery(params) {
|
|
41
|
+
const { south, west, north, east, tagKey, tagValue, elementTypes, timeoutSeconds } = params;
|
|
42
|
+
// Overpass bbox order: south,west,north,east (latitude-first)
|
|
43
|
+
const filter = `(${south},${west},${north},${east})`;
|
|
44
|
+
const tagFilter = `["${tagKey}"="${tagValue}"]`;
|
|
45
|
+
const lines = [
|
|
46
|
+
`[out:json][timeout:${timeoutSeconds}];`,
|
|
47
|
+
'(',
|
|
48
|
+
...elementTypes.map((t) => ` ${t}${tagFilter}${filter};`),
|
|
49
|
+
');',
|
|
50
|
+
'out center tags;',
|
|
51
|
+
];
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
async executeQuery(query, ctx) {
|
|
55
|
+
const cacheKey = this.buildCacheKey(query);
|
|
56
|
+
const cached = await ctx.state.get(cacheKey);
|
|
57
|
+
if (cached !== null) {
|
|
58
|
+
ctx.log.debug('Overpass cache hit');
|
|
59
|
+
return cached;
|
|
60
|
+
}
|
|
61
|
+
const result = await withRetry(async () => {
|
|
62
|
+
const response = await fetch(this.endpoint(), {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
66
|
+
'User-Agent': getServerConfig().nominatimUserAgent,
|
|
67
|
+
},
|
|
68
|
+
body: `data=${encodeURIComponent(query)}`,
|
|
69
|
+
signal: ctx.signal,
|
|
70
|
+
});
|
|
71
|
+
if (response.status === 429) {
|
|
72
|
+
throw serviceUnavailable('Overpass API returned HTTP 429 — all query slots occupied.', {
|
|
73
|
+
reason: 'rate_limited',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (response.status === 400) {
|
|
77
|
+
const body = await response.text();
|
|
78
|
+
throw serviceUnavailable(`Overpass API returned HTTP 400 — malformed query syntax. ${body.slice(0, 200)}`, { reason: 'query_error' });
|
|
79
|
+
}
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw serviceUnavailable(`Overpass API returned HTTP ${response.status}`, {
|
|
82
|
+
status: response.status,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const text = await response.text();
|
|
86
|
+
if (/^\s*<(!DOCTYPE\s+html|html[\s>])/i.test(text)) {
|
|
87
|
+
throw serviceUnavailable('Overpass returned an HTML page instead of JSON — likely rate-limited.');
|
|
88
|
+
}
|
|
89
|
+
const data = JSON.parse(text);
|
|
90
|
+
// Detect runtime errors embedded in JSON response
|
|
91
|
+
if (data.remark) {
|
|
92
|
+
if (OVERPASS_TIMEOUT_PATTERN.test(data.remark)) {
|
|
93
|
+
throw timeoutError(`Overpass query timed out: ${data.remark}`, {
|
|
94
|
+
reason: 'query_timeout',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (OVERPASS_OOM_PATTERN.test(data.remark)) {
|
|
98
|
+
throw serviceUnavailable(`Overpass ran out of memory: ${data.remark}`, {
|
|
99
|
+
reason: 'result_too_large',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return data;
|
|
104
|
+
}, {
|
|
105
|
+
operation: 'overpass.query',
|
|
106
|
+
context: ctx,
|
|
107
|
+
baseDelayMs: 2000,
|
|
108
|
+
signal: ctx.signal,
|
|
109
|
+
});
|
|
110
|
+
await ctx.state.set(cacheKey, result, { ttl: CACHE_TTL_SECONDS });
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
/** Execute a generated or raw Overpass QL query and return raw elements. */
|
|
114
|
+
query(ql, ctx) {
|
|
115
|
+
ctx.log.info('Overpass query', { queryLength: ql.length });
|
|
116
|
+
return this.executeQuery(ql, ctx);
|
|
117
|
+
}
|
|
118
|
+
/** Normalize Overpass elements into POI-friendly shape. */
|
|
119
|
+
normalizeElements(elements) {
|
|
120
|
+
return elements.map((el) => {
|
|
121
|
+
const lat = el.type === 'node' ? el.lat : el.center?.lat;
|
|
122
|
+
const lon = el.type === 'node' ? el.lon : el.center?.lon;
|
|
123
|
+
const tags = el.tags ?? {};
|
|
124
|
+
return {
|
|
125
|
+
osm_type: el.type,
|
|
126
|
+
osm_id: el.id,
|
|
127
|
+
...(lat !== undefined && { lat }),
|
|
128
|
+
...(lon !== undefined && { lon }),
|
|
129
|
+
...(tags.name ? { name: tags.name } : {}),
|
|
130
|
+
tags,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// --- Init/accessor pattern ---
|
|
136
|
+
let _service;
|
|
137
|
+
export function initOverpassService(config, storage) {
|
|
138
|
+
_service = new OverpassService(config, storage);
|
|
139
|
+
}
|
|
140
|
+
export function getOverpassService() {
|
|
141
|
+
if (!_service) {
|
|
142
|
+
throw new Error('OverpassService not initialized — call initOverpassService() in setup()');
|
|
143
|
+
}
|
|
144
|
+
return _service;
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=overpass-service.js.map
|