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