@happyvertical/geo 0.79.0 → 0.80.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT.md +1 -1
- package/dist/chunks/google-CZUa1ho0.js +244 -0
- package/dist/chunks/google-CZUa1ho0.js.map +1 -0
- package/dist/chunks/openstreetmap-St4Kw-vS.js +314 -0
- package/dist/chunks/openstreetmap-St4Kw-vS.js.map +1 -0
- package/dist/cli/claude-context.js +17 -17
- package/dist/cli/claude-context.js.map +1 -1
- package/dist/index.js +377 -261
- package/dist/index.js.map +1 -1
- package/metadata.json +1 -1
- package/package.json +6 -6
- package/dist/chunks/google-Ci3_ec7t.js +0 -342
- package/dist/chunks/google-Ci3_ec7t.js.map +0 -1
- package/dist/chunks/openstreetmap-DEPHzMUV.js +0 -419
- package/dist/chunks/openstreetmap-DEPHzMUV.js.map +0 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/shared/types.ts","../src/shared/utils.ts","../src/static-maps.ts","../src/index.ts"],"sourcesContent":["/**\n * Core types and interfaces for the Geo library\n */\n\n/**\n * Standardized location data structure\n */\nexport interface Location {\n /**\n * Unique identifier for the location (from provider)\n */\n id: string;\n\n /**\n * Type of location\n */\n type:\n | 'country'\n | 'region'\n | 'city'\n | 'address'\n | 'point_of_interest'\n | 'unknown';\n\n /**\n * Full formatted name/address of the location\n */\n name: string;\n\n /**\n * Latitude coordinate\n */\n latitude: number;\n\n /**\n * Longitude coordinate\n */\n longitude: number;\n\n /**\n * Address components (all optional)\n */\n addressComponents: {\n streetNumber?: string;\n streetName?: string;\n city?: string;\n region?: string;\n country?: string;\n postalCode?: string;\n };\n\n /**\n * ISO 3166-1 alpha-2 country code\n */\n countryCode: string;\n\n /**\n * Timezone identifier (optional, populated when provider returns it)\n */\n timezone?: string;\n\n /**\n * Raw response from the provider (for debugging or provider-specific data)\n */\n raw: any;\n}\n\n/**\n * Options for POI (point-of-interest) searches.\n *\n * `types` and `keyword` are both forwarded to the backing provider but each\n * provider interprets them slightly differently:\n *\n * - **Google**: `types[0]` becomes the request's `type` filter (Places API\n * accepts a single type per request; additional entries are ignored).\n * `keyword` is a free-text match across name/type/address/reviews.\n * - **OpenStreetMap (Overpass)**: `types` are matched against\n * `amenity`, `shop`, and `tourism` tag values (e.g. `'cafe'`,\n * `'supermarket'`, `'museum'`). When omitted, the provider searches across\n * a broad set of POI-ish tag keys (`amenity`, `shop`, `tourism`,\n * `leisure`, `office`, `historic`). `keyword` is appended as a substring\n * filter on the `name` tag.\n */\nexport interface PoiSearchOptions {\n /** Filter results to POIs matching these category values. See notes above. */\n types?: string[];\n /** Free-text keyword to narrow the search. */\n keyword?: string;\n /** Max results to return. Default 20. */\n limit?: number;\n /** Preferred language for place names (Google only). */\n language?: string;\n}\n\n/**\n * Geo provider interface - all providers must implement lookup and\n * reverseGeocode. `findPoisNear` is optional — providers implement it when\n * they support POI discovery beyond reverse geocoding. Callers that need to\n * know whether a given instance supports POI search should feature-detect:\n *\n * ```ts\n * if (typeof adapter.findPoisNear === 'function') { ... }\n * ```\n */\nexport interface GeoProvider {\n /**\n * Look up locations based on a query string\n * @param query - Search string (address, city, country, POI, etc.)\n * @returns Promise resolving to array of matching Location objects\n */\n lookup(query: string): Promise<Location[]>;\n\n /**\n * Reverse geocode from coordinates to location\n * @param latitude - Latitude coordinate\n * @param longitude - Longitude coordinate\n * @returns Promise resolving to array of matching Location objects\n */\n reverseGeocode(latitude: number, longitude: number): Promise<Location[]>;\n\n /**\n * Find POIs (point-of-interest places — businesses, landmarks, amenities)\n * within a radius of a coordinate. Returns locations with\n * `type: 'point_of_interest'` whose coords lie inside the requested\n * radius, sorted by the provider's relevance ranking.\n *\n * @param latitude - Center latitude\n * @param longitude - Center longitude\n * @param radiusMeters - Search radius in meters\n * @param options - Optional filters\n * @returns Promise resolving to array of matching Location objects\n */\n findPoisNear?(\n latitude: number,\n longitude: number,\n radiusMeters: number,\n options?: PoiSearchOptions,\n ): Promise<Location[]>;\n}\n\n/**\n * Geo adapter interface (structurally identical to GeoProvider)\n */\nexport interface GeoAdapter {\n /**\n * Look up locations based on a query string\n * @param query - Search string (address, city, country, POI, etc.)\n * @returns Promise resolving to array of matching Location objects\n */\n lookup(query: string): Promise<Location[]>;\n\n /**\n * Reverse geocode from coordinates to location\n * @param latitude - Latitude coordinate\n * @param longitude - Longitude coordinate\n * @returns Promise resolving to array of matching Location objects\n */\n reverseGeocode(latitude: number, longitude: number): Promise<Location[]>;\n\n /**\n * Find POIs near a coordinate. Optional — see `GeoProvider.findPoisNear`.\n */\n findPoisNear?(\n latitude: number,\n longitude: number,\n radiusMeters: number,\n options?: PoiSearchOptions,\n ): Promise<Location[]>;\n}\n\n/**\n * Base configuration options for all providers\n */\nexport interface BaseGeoOptions {\n /**\n * Request timeout in milliseconds\n */\n timeout?: number;\n\n /**\n * Maximum number of results to return\n */\n maxResults?: number;\n}\n\n/**\n * Google Maps provider options\n */\nexport interface GoogleMapsOptions extends BaseGeoOptions {\n provider: 'google';\n /**\n * Google Maps API key\n */\n apiKey: string;\n}\n\n/**\n * OpenStreetMap provider options\n */\nexport interface OpenStreetMapOptions extends BaseGeoOptions {\n provider: 'openstreetmap';\n /**\n * Custom User-Agent for OSM requests (optional, defaults to package name)\n */\n userAgent?: string;\n /**\n * Rate limit delay in milliseconds (default: 1000ms for 1 req/sec)\n */\n rateLimitDelay?: number;\n}\n\n/**\n * Union type for all provider options\n */\nexport type GeoAdapterOptions = GoogleMapsOptions | OpenStreetMapOptions;\n\n/**\n * Base error class for geo operations.\n * All geo-specific errors extend this class and include a machine-readable `code`\n * and the `provider` name that produced the error.\n */\nexport class GeoError extends Error {\n /**\n * @param message - Human-readable error description\n * @param code - Machine-readable error code (e.g. 'RATE_LIMIT', 'AUTH_ERROR')\n * @param provider - Provider that raised the error ('google' | 'openstreetmap')\n */\n constructor(\n message: string,\n public code: string,\n public provider?: string,\n ) {\n super(message);\n this.name = 'GeoError';\n }\n}\n\n/**\n * Thrown when the provider's rate limit has been exceeded.\n */\nexport class RateLimitError extends GeoError {\n /**\n * @param provider - Provider that raised the error\n * @param retryAfter - Seconds to wait before retrying (if known)\n */\n constructor(provider?: string, retryAfter?: number) {\n super(\n `Rate limit exceeded${retryAfter ? `, retry after ${retryAfter}s` : ''}`,\n 'RATE_LIMIT',\n provider,\n );\n this.name = 'RateLimitError';\n }\n}\n\n/**\n * Thrown when the geocoding query is empty or malformed.\n */\nexport class InvalidQueryError extends GeoError {\n /**\n * @param query - The invalid query string\n * @param provider - Provider that raised the error\n */\n constructor(query: string, provider?: string) {\n super(`Invalid query: ${query}`, 'INVALID_QUERY', provider);\n this.name = 'InvalidQueryError';\n }\n}\n\n/**\n * Thrown when the provider rejects the API key or credentials.\n */\nexport class AuthenticationError extends GeoError {\n /**\n * @param provider - Provider that raised the error\n */\n constructor(provider?: string) {\n super('Authentication failed', 'AUTH_ERROR', provider);\n this.name = 'AuthenticationError';\n }\n}\n\n/**\n * Thrown when a geocoding query returns zero results.\n */\nexport class NoResultsError extends GeoError {\n /**\n * @param query - The query that produced no results\n * @param provider - Provider that raised the error\n */\n constructor(query: string, provider?: string) {\n super(`No results found for query: ${query}`, 'NO_RESULTS', provider);\n this.name = 'NoResultsError';\n }\n}\n","/**\n * Utility functions for geo operations\n */\n\nimport type { Location } from './types';\n\n/**\n * Map Google Maps place types to standardized location types.\n * Checks types in priority order: address, city, region, country, point_of_interest.\n *\n * @param types - Array of Google Maps place type strings (e.g. 'street_address', 'locality')\n * @returns The standardized location type\n */\nexport function mapGooglePlaceType(types: string[]): Location['type'] {\n if (!types || types.length === 0) return 'unknown';\n\n // Check in priority order\n if (types.includes('street_address') || types.includes('premise')) {\n return 'address';\n }\n if (types.includes('locality') || types.includes('postal_town')) {\n return 'city';\n }\n if (\n types.includes('administrative_area_level_1') ||\n types.includes('administrative_area_level_2')\n ) {\n return 'region';\n }\n if (types.includes('country')) {\n return 'country';\n }\n if (types.includes('point_of_interest') || types.includes('establishment')) {\n return 'point_of_interest';\n }\n\n return 'unknown';\n}\n\n/**\n * Map OpenStreetMap place types to standardized location types.\n * Uses both the Nominatim `type` and `addresstype` fields for classification.\n *\n * @param type - Nominatim result type (e.g. 'house', 'city', 'state')\n * @param addressType - Nominatim addresstype field (optional secondary classifier)\n * @returns The standardized location type\n */\nexport function mapOSMPlaceType(\n type: string,\n addressType?: string,\n): Location['type'] {\n // OSM uses different classification - check both type and addresstype\n const checkType = (type || '').toLowerCase();\n const checkAddressType = (addressType || '').toLowerCase();\n\n if (\n checkType === 'house' ||\n checkType === 'building' ||\n checkAddressType === 'house'\n ) {\n return 'address';\n }\n\n if (\n checkType === 'city' ||\n checkType === 'town' ||\n checkType === 'village' ||\n checkType === 'hamlet' ||\n checkAddressType === 'city' ||\n checkAddressType === 'town' ||\n checkAddressType === 'village'\n ) {\n return 'city';\n }\n\n if (\n checkType === 'state' ||\n checkType === 'province' ||\n checkType === 'region' ||\n checkAddressType === 'state'\n ) {\n return 'region';\n }\n\n if (checkType === 'country' || checkAddressType === 'country') {\n return 'country';\n }\n\n if (\n checkType === 'attraction' ||\n checkType === 'tourism' ||\n checkType === 'amenity'\n ) {\n return 'point_of_interest';\n }\n\n return 'unknown';\n}\n\n/**\n * Normalize a country code to uppercase ISO 3166-1 format.\n * Returns 'XX' for undefined, empty, or unrecognized inputs.\n *\n * @param code - Country code string (alpha-2 or alpha-3)\n * @returns Uppercase country code, or 'XX' if unknown\n */\nexport function normalizeCountryCode(code: string | undefined): string {\n if (!code) return 'XX'; // Unknown country code\n\n // Already uppercase 2-letter code\n const normalized = code.toUpperCase().trim();\n\n // If it's already 2 letters, return it\n if (normalized.length === 2) {\n return normalized;\n }\n\n // If it's 3 letters (alpha-3), we would need a mapping table\n // For now, return as-is or 'XX' for unknown\n return normalized.length === 3 ? normalized : 'XX';\n}\n\n/**\n * Validate that a latitude value is within the valid range (-90 to 90).\n *\n * @param lat - Latitude value to validate\n * @returns `true` if the value is a number between -90 and 90 inclusive\n */\nexport function isValidLatitude(lat: number): boolean {\n return typeof lat === 'number' && lat >= -90 && lat <= 90;\n}\n\n/**\n * Validate that a longitude value is within the valid range (-180 to 180).\n *\n * @param lng - Longitude value to validate\n * @returns `true` if the value is a number between -180 and 180 inclusive\n */\nexport function isValidLongitude(lng: number): boolean {\n return typeof lng === 'number' && lng >= -180 && lng <= 180;\n}\n\n/**\n * Validate that both latitude and longitude are within their valid ranges.\n *\n * @param latitude - Latitude value (-90 to 90)\n * @param longitude - Longitude value (-180 to 180)\n * @returns Object with `valid: true` if both are valid, or `valid: false` with an `error` message\n */\nexport function validateCoordinates(\n latitude: number,\n longitude: number,\n): { valid: boolean; error?: string } {\n if (!isValidLatitude(latitude)) {\n return {\n valid: false,\n error: `Invalid latitude: ${latitude}. Must be between -90 and 90.`,\n };\n }\n\n if (!isValidLongitude(longitude)) {\n return {\n valid: false,\n error: `Invalid longitude: ${longitude}. Must be between -180 and 180.`,\n };\n }\n\n return { valid: true };\n}\n","/**\n * Static Map Generation\n *\n * Generates static map URLs and fetches map images for location-based content.\n * Supports Mapbox and Google Maps providers.\n *\n * @example\n * ```typescript\n * import { getStaticMapUrl, fetchStaticMap } from '@happyvertical/geo';\n *\n * // Get URL for embedding\n * const url = getStaticMapUrl(53.5461, -113.4938, {\n * provider: 'mapbox',\n * zoom: 14,\n * width: 1200,\n * height: 630\n * });\n *\n * // Fetch the actual image\n * const buffer = await fetchStaticMap(53.5461, -113.4938, {\n * provider: 'mapbox'\n * });\n * await writeFile('map.png', buffer);\n * ```\n */\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Static map provider\n */\nexport type StaticMapProvider = 'mapbox' | 'google';\n\n/**\n * Mapbox map styles\n * @see https://docs.mapbox.com/api/maps/styles/\n */\nexport type MapboxStyle =\n | 'streets-v12'\n | 'outdoors-v12'\n | 'light-v11'\n | 'dark-v11'\n | 'satellite-v9'\n | 'satellite-streets-v12';\n\n/**\n * Google Maps map types\n * @see https://developers.google.com/maps/documentation/maps-static/start\n */\nexport type GoogleMapType = 'roadmap' | 'satellite' | 'terrain' | 'hybrid';\n\n/**\n * Marker definition for the map\n */\nexport interface StaticMapMarker {\n /** Latitude */\n latitude: number;\n /** Longitude */\n longitude: number;\n /** Marker color (hex or named color) */\n color?: string;\n /** Marker label (single character for Google, any for Mapbox) */\n label?: string;\n /** Marker size ('small' | 'medium' | 'large') */\n size?: 'small' | 'medium' | 'large';\n}\n\n/**\n * Options for static map generation\n */\nexport interface StaticMapOptions {\n /**\n * Map provider\n * @default 'mapbox'\n */\n provider?: StaticMapProvider;\n\n /**\n * Image width in pixels\n * @default 1200\n */\n width?: number;\n\n /**\n * Image height in pixels\n * @default 630\n */\n height?: number;\n\n /**\n * Zoom level (1-20)\n * @default 14\n */\n zoom?: number;\n\n /**\n * Marker color (hex without # or named color)\n * @default 'e74c3c' (red)\n */\n markerColor?: string;\n\n /**\n * Show a marker at the center point\n * @default true\n */\n showMarker?: boolean;\n\n /**\n * Mapbox access token (required for Mapbox provider)\n * Falls back to MAPBOX_ACCESS_TOKEN env var\n */\n mapboxToken?: string;\n\n /**\n * Google Maps API key (required for Google provider)\n * Falls back to GOOGLE_MAPS_API_KEY env var\n */\n googleApiKey?: string;\n\n /**\n * Mapbox style\n * @default 'streets-v12'\n */\n mapboxStyle?: MapboxStyle;\n\n /**\n * Google map type\n * @default 'roadmap'\n */\n googleMapType?: GoogleMapType;\n\n /**\n * Pixel density (1 or 2 for retina)\n * @default 1\n */\n scale?: 1 | 2;\n\n /**\n * Additional markers to display\n */\n markers?: StaticMapMarker[];\n}\n\n/**\n * Result from fetching a static map\n */\nexport interface StaticMapResult {\n /** PNG image buffer */\n buffer: Buffer;\n /** Image width */\n width: number;\n /** Image height */\n height: number;\n /** MIME type */\n mimeType: 'image/png';\n /** The URL that was fetched */\n url: string;\n}\n\n// ============================================================================\n// URL Generators\n// ============================================================================\n\n/**\n * Generate Mapbox static map URL\n */\nfunction getMapboxUrl(\n latitude: number,\n longitude: number,\n options: StaticMapOptions,\n): string {\n const token =\n options.mapboxToken ??\n process.env.MAPBOX_ACCESS_TOKEN ??\n process.env.MAPBOX_TOKEN;\n if (!token) {\n throw new Error(\n 'Mapbox access token required. Provide via options.mapboxToken or MAPBOX_ACCESS_TOKEN env var.',\n );\n }\n\n const width = options.width ?? 1200;\n const height = options.height ?? 630;\n const zoom = options.zoom ?? 14;\n const style = options.mapboxStyle ?? 'streets-v12';\n const scale = options.scale ?? 1;\n const showMarker = options.showMarker ?? true;\n\n // Build overlay for marker\n let overlay = '';\n if (showMarker) {\n const color = (options.markerColor ?? 'e74c3c').replace('#', '');\n // Mapbox marker format: pin-{size}-{label}+{color}({lon},{lat})\n overlay = `pin-l+${color}(${longitude},${latitude})/`;\n }\n\n // Add additional markers\n if (options.markers?.length) {\n const markerOverlays = options.markers.map((m) => {\n const color = (m.color ?? 'e74c3c').replace('#', '');\n const size = m.size === 'small' ? 's' : m.size === 'large' ? 'l' : 'm';\n const label = m.label ? `-${m.label}` : '';\n return `pin-${size}${label}+${color}(${m.longitude},${m.latitude})`;\n });\n overlay = markerOverlays.join(',') + '/';\n }\n\n const retinaStr = scale === 2 ? '@2x' : '';\n\n return `https://api.mapbox.com/styles/v1/mapbox/${style}/static/${overlay}${longitude},${latitude},${zoom}/${width}x${height}${retinaStr}?access_token=${token}`;\n}\n\n/**\n * Generate Google Maps static map URL\n */\nfunction getGoogleUrl(\n latitude: number,\n longitude: number,\n options: StaticMapOptions,\n): string {\n const apiKey =\n options.googleApiKey ??\n process.env.GOOGLE_MAPS_API_KEY ??\n process.env.GOOGLE_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'Google Maps API key required. Provide via options.googleApiKey or GOOGLE_MAPS_API_KEY env var.',\n );\n }\n\n const width = options.width ?? 1200;\n const height = options.height ?? 630;\n const zoom = options.zoom ?? 14;\n const mapType = options.googleMapType ?? 'roadmap';\n const scale = options.scale ?? 1;\n const showMarker = options.showMarker ?? true;\n\n const params = new URLSearchParams({\n center: `${latitude},${longitude}`,\n zoom: zoom.toString(),\n size: `${width}x${height}`,\n maptype: mapType,\n scale: scale.toString(),\n key: apiKey,\n });\n\n // Add center marker\n if (showMarker) {\n const color = (options.markerColor ?? 'red').replace('#', '0x');\n params.append('markers', `color:${color}|${latitude},${longitude}`);\n }\n\n // Add additional markers\n if (options.markers?.length) {\n for (const m of options.markers) {\n const color = (m.color ?? 'red').replace('#', '0x');\n const size = m.size ?? 'medium';\n const label = m.label ? `|label:${m.label.charAt(0).toUpperCase()}` : '';\n params.append(\n 'markers',\n `color:${color}|size:${size}${label}|${m.latitude},${m.longitude}`,\n );\n }\n }\n\n return `https://maps.googleapis.com/maps/api/staticmap?${params.toString()}`;\n}\n\n// ============================================================================\n// Main Functions\n// ============================================================================\n\n/**\n * Generate a static map URL\n *\n * Returns a URL that can be used directly in <img> tags or fetched for processing.\n *\n * @param latitude - Center latitude\n * @param longitude - Center longitude\n * @param options - Map options\n * @returns URL string for the static map\n *\n * @example Mapbox (default)\n * ```typescript\n * const url = getStaticMapUrl(53.5461, -113.4938, {\n * provider: 'mapbox',\n * zoom: 14,\n * mapboxStyle: 'streets-v12'\n * });\n * ```\n *\n * @example Google Maps\n * ```typescript\n * const url = getStaticMapUrl(53.5461, -113.4938, {\n * provider: 'google',\n * zoom: 14,\n * googleMapType: 'roadmap'\n * });\n * ```\n *\n * @example With custom markers\n * ```typescript\n * const url = getStaticMapUrl(53.5461, -113.4938, {\n * showMarker: false, // Hide center marker\n * markers: [\n * { latitude: 53.5461, longitude: -113.4938, color: 'blue', label: 'A' },\n * { latitude: 53.5500, longitude: -113.4900, color: 'green', label: 'B' }\n * ]\n * });\n * ```\n */\nexport function getStaticMapUrl(\n latitude: number,\n longitude: number,\n options: StaticMapOptions = {},\n): string {\n const provider = options.provider ?? 'mapbox';\n\n switch (provider) {\n case 'google':\n return getGoogleUrl(latitude, longitude, options);\n case 'mapbox':\n default:\n return getMapboxUrl(latitude, longitude, options);\n }\n}\n\n/**\n * Fetch a static map image\n *\n * Downloads the map image and returns it as a Buffer.\n *\n * @param latitude - Center latitude\n * @param longitude - Center longitude\n * @param options - Map options\n * @returns Promise resolving to the map image result\n *\n * @example\n * ```typescript\n * const result = await fetchStaticMap(53.5461, -113.4938, {\n * provider: 'mapbox',\n * width: 1200,\n * height: 630\n * });\n *\n * await fs.writeFile('map.png', result.buffer);\n * console.log(`Fetched ${result.width}x${result.height} map`);\n * ```\n */\nexport async function fetchStaticMap(\n latitude: number,\n longitude: number,\n options: StaticMapOptions = {},\n): Promise<StaticMapResult> {\n const width = options.width ?? 1200;\n const height = options.height ?? 630;\n const url = getStaticMapUrl(latitude, longitude, options);\n\n const response = await fetch(url);\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Failed to fetch static map: ${response.status} ${response.statusText} - ${errorText}`,\n );\n }\n\n const arrayBuffer = await response.arrayBuffer();\n const buffer = Buffer.from(arrayBuffer);\n\n return {\n buffer,\n width,\n height,\n mimeType: 'image/png',\n url,\n };\n}\n\n/**\n * Generate OG-sized static map URL (1200x630)\n *\n * Convenience function for generating Open Graph / social media sized maps.\n *\n * @param latitude - Center latitude\n * @param longitude - Center longitude\n * @param options - Map options (width/height ignored, set to 1200x630)\n * @returns URL string for the static map\n *\n * @example\n * ```typescript\n * const url = getOGMapUrl(53.5461, -113.4938);\n * // Returns 1200x630 map URL\n * ```\n */\nexport function getOGMapUrl(\n latitude: number,\n longitude: number,\n options: Omit<StaticMapOptions, 'width' | 'height'> = {},\n): string {\n return getStaticMapUrl(latitude, longitude, {\n ...options,\n width: 1200,\n height: 630,\n });\n}\n","/**\n * Geo package entry point\n * Provides standardized geographical information interface\n */\n\nimport { loadEnvConfig } from '@happyvertical/utils';\nimport type {\n GeoAdapter,\n GeoAdapterOptions,\n GoogleMapsOptions,\n OpenStreetMapOptions,\n} from './shared/types';\n\n// Export all types\nexport * from './shared/types';\nexport * from './shared/utils';\n\n// Export static map generation\nexport {\n fetchStaticMap,\n type GoogleMapType,\n getOGMapUrl,\n getStaticMapUrl,\n type MapboxStyle,\n type StaticMapMarker,\n type StaticMapOptions,\n type StaticMapProvider,\n type StaticMapResult,\n} from './static-maps';\n\n/**\n * Type guard for Google Maps options\n */\nfunction isGoogleMapsOptions(\n options: GeoAdapterOptions,\n): options is GoogleMapsOptions {\n return options.provider === 'google';\n}\n\n/**\n * Type guard for OpenStreetMap options\n */\nfunction isOpenStreetMapOptions(\n options: GeoAdapterOptions,\n): options is OpenStreetMapOptions {\n return options.provider === 'openstreetmap';\n}\n\n/**\n * Factory function to create a geo adapter instance\n *\n * Supports configuration via environment variables using the pattern:\n * - HAVE_GEO_PROVIDER → provider ('google' | 'openstreetmap')\n * - GOOGLE_MAPS_API_KEY → apiKey (for Google Maps provider)\n *\n * User-provided options always take precedence over environment variables.\n *\n * @param options - Configuration options for the geo provider (optional)\n * @returns Promise resolving to a geo adapter that implements GeoAdapter\n *\n * @example\n * ```typescript\n * // Create Google Maps adapter with explicit options\n * const googleGeo = await getGeoAdapter({\n * provider: 'google',\n * apiKey: process.env.GOOGLE_MAPS_API_KEY!\n * });\n *\n * // Create adapter using environment variables\n * // HAVE_GEO_PROVIDER=google\n * // GOOGLE_MAPS_API_KEY=your-api-key\n * const geoFromEnv = await getGeoAdapter();\n *\n * // Create OpenStreetMap adapter\n * const osmGeo = await getGeoAdapter({\n * provider: 'openstreetmap'\n * });\n *\n * // Use the adapter\n * const locations = await googleGeo.lookup('Eiffel Tower');\n * const coords = await osmGeo.reverseGeocode(48.8584, 2.2945);\n * ```\n */\nexport async function getGeoAdapter(\n options: Partial<GeoAdapterOptions> = {},\n): Promise<GeoAdapter> {\n // Load configuration from environment variables\n // Cast to any to handle union type with loadEnvConfig\n const config = loadEnvConfig(options as any, {\n packageName: 'geo',\n schema: {\n provider: 'string',\n timeout: 'number',\n maxResults: 'number',\n rateLimitDelay: 'number',\n userAgent: 'string',\n },\n }) as Partial<GeoAdapterOptions>;\n\n // Handle Google Maps API key from environment (not using HAVE_GEO_ prefix)\n if (config.provider === 'google' && !config.apiKey) {\n const apiKey = process.env.GOOGLE_MAPS_API_KEY;\n if (apiKey) {\n config.apiKey = apiKey;\n }\n }\n\n // Validate that we have a provider\n if (!config.provider) {\n throw new Error(\n 'Provider is required. Set via options.provider or HAVE_GEO_PROVIDER environment variable.',\n );\n }\n\n // Cast to full options type for provider-specific handling\n const fullOptions = config as GeoAdapterOptions;\n\n if (isGoogleMapsOptions(fullOptions)) {\n const { GoogleMapsProvider } = await import('./providers/google.js');\n return new GoogleMapsProvider(fullOptions);\n }\n\n if (isOpenStreetMapOptions(fullOptions)) {\n const { OpenStreetMapProvider } = await import(\n './providers/openstreetmap.js'\n );\n return new OpenStreetMapProvider(fullOptions);\n }\n\n // This should never happen due to TypeScript's discriminated union\n throw new Error(`Unsupported provider: ${(fullOptions as any).provider}`);\n}\n\n/** @internal */\nexport const PACKAGE_VERSION_INITIALIZED = true;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/shared/types.ts","../src/shared/utils.ts","../src/static-maps.ts","../src/index.ts"],"sourcesContent":["/**\n * Core types and interfaces for the Geo library\n */\n\n/**\n * Standardized location data structure\n */\nexport interface Location {\n /**\n * Unique identifier for the location (from provider)\n */\n id: string;\n\n /**\n * Type of location\n */\n type:\n | 'country'\n | 'region'\n | 'city'\n | 'address'\n | 'point_of_interest'\n | 'unknown';\n\n /**\n * Full formatted name/address of the location\n */\n name: string;\n\n /**\n * Latitude coordinate\n */\n latitude: number;\n\n /**\n * Longitude coordinate\n */\n longitude: number;\n\n /**\n * Address components (all optional)\n */\n addressComponents: {\n streetNumber?: string;\n streetName?: string;\n city?: string;\n region?: string;\n country?: string;\n postalCode?: string;\n };\n\n /**\n * ISO 3166-1 alpha-2 country code\n */\n countryCode: string;\n\n /**\n * Timezone identifier (optional, populated when provider returns it)\n */\n timezone?: string;\n\n /**\n * Raw response from the provider (for debugging or provider-specific data)\n */\n raw: any;\n}\n\n/**\n * Options for POI (point-of-interest) searches.\n *\n * `types` and `keyword` are both forwarded to the backing provider but each\n * provider interprets them slightly differently:\n *\n * - **Google**: `types[0]` becomes the request's `type` filter (Places API\n * accepts a single type per request; additional entries are ignored).\n * `keyword` is a free-text match across name/type/address/reviews.\n * - **OpenStreetMap (Overpass)**: `types` are matched against\n * `amenity`, `shop`, and `tourism` tag values (e.g. `'cafe'`,\n * `'supermarket'`, `'museum'`). When omitted, the provider searches across\n * a broad set of POI-ish tag keys (`amenity`, `shop`, `tourism`,\n * `leisure`, `office`, `historic`). `keyword` is appended as a substring\n * filter on the `name` tag.\n */\nexport interface PoiSearchOptions {\n /** Filter results to POIs matching these category values. See notes above. */\n types?: string[];\n /** Free-text keyword to narrow the search. */\n keyword?: string;\n /** Max results to return. Default 20. */\n limit?: number;\n /** Preferred language for place names (Google only). */\n language?: string;\n}\n\n/**\n * Geo provider interface - all providers must implement lookup and\n * reverseGeocode. `findPoisNear` is optional — providers implement it when\n * they support POI discovery beyond reverse geocoding. Callers that need to\n * know whether a given instance supports POI search should feature-detect:\n *\n * ```ts\n * if (typeof adapter.findPoisNear === 'function') { ... }\n * ```\n */\nexport interface GeoProvider {\n /**\n * Look up locations based on a query string\n * @param query - Search string (address, city, country, POI, etc.)\n * @returns Promise resolving to array of matching Location objects\n */\n lookup(query: string): Promise<Location[]>;\n\n /**\n * Reverse geocode from coordinates to location\n * @param latitude - Latitude coordinate\n * @param longitude - Longitude coordinate\n * @returns Promise resolving to array of matching Location objects\n */\n reverseGeocode(latitude: number, longitude: number): Promise<Location[]>;\n\n /**\n * Find POIs (point-of-interest places — businesses, landmarks, amenities)\n * within a radius of a coordinate. Returns locations with\n * `type: 'point_of_interest'` whose coords lie inside the requested\n * radius, sorted by the provider's relevance ranking.\n *\n * @param latitude - Center latitude\n * @param longitude - Center longitude\n * @param radiusMeters - Search radius in meters\n * @param options - Optional filters\n * @returns Promise resolving to array of matching Location objects\n */\n findPoisNear?(\n latitude: number,\n longitude: number,\n radiusMeters: number,\n options?: PoiSearchOptions,\n ): Promise<Location[]>;\n}\n\n/**\n * Geo adapter interface (structurally identical to GeoProvider)\n */\nexport interface GeoAdapter {\n /**\n * Look up locations based on a query string\n * @param query - Search string (address, city, country, POI, etc.)\n * @returns Promise resolving to array of matching Location objects\n */\n lookup(query: string): Promise<Location[]>;\n\n /**\n * Reverse geocode from coordinates to location\n * @param latitude - Latitude coordinate\n * @param longitude - Longitude coordinate\n * @returns Promise resolving to array of matching Location objects\n */\n reverseGeocode(latitude: number, longitude: number): Promise<Location[]>;\n\n /**\n * Find POIs near a coordinate. Optional — see `GeoProvider.findPoisNear`.\n */\n findPoisNear?(\n latitude: number,\n longitude: number,\n radiusMeters: number,\n options?: PoiSearchOptions,\n ): Promise<Location[]>;\n}\n\n/**\n * Base configuration options for all providers\n */\nexport interface BaseGeoOptions {\n /**\n * Request timeout in milliseconds\n */\n timeout?: number;\n\n /**\n * Maximum number of results to return\n */\n maxResults?: number;\n}\n\n/**\n * Google Maps provider options\n */\nexport interface GoogleMapsOptions extends BaseGeoOptions {\n provider: 'google';\n /**\n * Google Maps API key\n */\n apiKey: string;\n}\n\n/**\n * OpenStreetMap provider options\n */\nexport interface OpenStreetMapOptions extends BaseGeoOptions {\n provider: 'openstreetmap';\n /**\n * Custom User-Agent for OSM requests (optional, defaults to package name)\n */\n userAgent?: string;\n /**\n * Rate limit delay in milliseconds (default: 1000ms for 1 req/sec)\n */\n rateLimitDelay?: number;\n}\n\n/**\n * Union type for all provider options\n */\nexport type GeoAdapterOptions = GoogleMapsOptions | OpenStreetMapOptions;\n\n/**\n * Base error class for geo operations.\n * All geo-specific errors extend this class and include a machine-readable `code`\n * and the `provider` name that produced the error.\n */\nexport class GeoError extends Error {\n /**\n * @param message - Human-readable error description\n * @param code - Machine-readable error code (e.g. 'RATE_LIMIT', 'AUTH_ERROR')\n * @param provider - Provider that raised the error ('google' | 'openstreetmap')\n */\n constructor(\n message: string,\n public code: string,\n public provider?: string,\n ) {\n super(message);\n this.name = 'GeoError';\n }\n}\n\n/**\n * Thrown when the provider's rate limit has been exceeded.\n */\nexport class RateLimitError extends GeoError {\n /**\n * @param provider - Provider that raised the error\n * @param retryAfter - Seconds to wait before retrying (if known)\n */\n constructor(provider?: string, retryAfter?: number) {\n super(\n `Rate limit exceeded${retryAfter ? `, retry after ${retryAfter}s` : ''}`,\n 'RATE_LIMIT',\n provider,\n );\n this.name = 'RateLimitError';\n }\n}\n\n/**\n * Thrown when the geocoding query is empty or malformed.\n */\nexport class InvalidQueryError extends GeoError {\n /**\n * @param query - The invalid query string\n * @param provider - Provider that raised the error\n */\n constructor(query: string, provider?: string) {\n super(`Invalid query: ${query}`, 'INVALID_QUERY', provider);\n this.name = 'InvalidQueryError';\n }\n}\n\n/**\n * Thrown when the provider rejects the API key or credentials.\n */\nexport class AuthenticationError extends GeoError {\n /**\n * @param provider - Provider that raised the error\n */\n constructor(provider?: string) {\n super('Authentication failed', 'AUTH_ERROR', provider);\n this.name = 'AuthenticationError';\n }\n}\n\n/**\n * Thrown when a geocoding query returns zero results.\n */\nexport class NoResultsError extends GeoError {\n /**\n * @param query - The query that produced no results\n * @param provider - Provider that raised the error\n */\n constructor(query: string, provider?: string) {\n super(`No results found for query: ${query}`, 'NO_RESULTS', provider);\n this.name = 'NoResultsError';\n }\n}\n","/**\n * Utility functions for geo operations\n */\n\nimport type { Location } from './types';\n\n/**\n * Map Google Maps place types to standardized location types.\n * Checks types in priority order: address, city, region, country, point_of_interest.\n *\n * @param types - Array of Google Maps place type strings (e.g. 'street_address', 'locality')\n * @returns The standardized location type\n */\nexport function mapGooglePlaceType(types: string[]): Location['type'] {\n if (!types || types.length === 0) return 'unknown';\n\n // Check in priority order\n if (types.includes('street_address') || types.includes('premise')) {\n return 'address';\n }\n if (types.includes('locality') || types.includes('postal_town')) {\n return 'city';\n }\n if (\n types.includes('administrative_area_level_1') ||\n types.includes('administrative_area_level_2')\n ) {\n return 'region';\n }\n if (types.includes('country')) {\n return 'country';\n }\n if (types.includes('point_of_interest') || types.includes('establishment')) {\n return 'point_of_interest';\n }\n\n return 'unknown';\n}\n\n/**\n * Map OpenStreetMap place types to standardized location types.\n * Uses both the Nominatim `type` and `addresstype` fields for classification.\n *\n * @param type - Nominatim result type (e.g. 'house', 'city', 'state')\n * @param addressType - Nominatim addresstype field (optional secondary classifier)\n * @returns The standardized location type\n */\nexport function mapOSMPlaceType(\n type: string,\n addressType?: string,\n): Location['type'] {\n // OSM uses different classification - check both type and addresstype\n const checkType = (type || '').toLowerCase();\n const checkAddressType = (addressType || '').toLowerCase();\n\n if (\n checkType === 'house' ||\n checkType === 'building' ||\n checkAddressType === 'house'\n ) {\n return 'address';\n }\n\n if (\n checkType === 'city' ||\n checkType === 'town' ||\n checkType === 'village' ||\n checkType === 'hamlet' ||\n checkAddressType === 'city' ||\n checkAddressType === 'town' ||\n checkAddressType === 'village'\n ) {\n return 'city';\n }\n\n if (\n checkType === 'state' ||\n checkType === 'province' ||\n checkType === 'region' ||\n checkAddressType === 'state'\n ) {\n return 'region';\n }\n\n if (checkType === 'country' || checkAddressType === 'country') {\n return 'country';\n }\n\n if (\n checkType === 'attraction' ||\n checkType === 'tourism' ||\n checkType === 'amenity'\n ) {\n return 'point_of_interest';\n }\n\n return 'unknown';\n}\n\n/**\n * Normalize a country code to uppercase ISO 3166-1 format.\n * Returns 'XX' for undefined, empty, or unrecognized inputs.\n *\n * @param code - Country code string (alpha-2 or alpha-3)\n * @returns Uppercase country code, or 'XX' if unknown\n */\nexport function normalizeCountryCode(code: string | undefined): string {\n if (!code) return 'XX'; // Unknown country code\n\n // Already uppercase 2-letter code\n const normalized = code.toUpperCase().trim();\n\n // If it's already 2 letters, return it\n if (normalized.length === 2) {\n return normalized;\n }\n\n // If it's 3 letters (alpha-3), we would need a mapping table\n // For now, return as-is or 'XX' for unknown\n return normalized.length === 3 ? normalized : 'XX';\n}\n\n/**\n * Validate that a latitude value is within the valid range (-90 to 90).\n *\n * @param lat - Latitude value to validate\n * @returns `true` if the value is a number between -90 and 90 inclusive\n */\nexport function isValidLatitude(lat: number): boolean {\n return typeof lat === 'number' && lat >= -90 && lat <= 90;\n}\n\n/**\n * Validate that a longitude value is within the valid range (-180 to 180).\n *\n * @param lng - Longitude value to validate\n * @returns `true` if the value is a number between -180 and 180 inclusive\n */\nexport function isValidLongitude(lng: number): boolean {\n return typeof lng === 'number' && lng >= -180 && lng <= 180;\n}\n\n/**\n * Validate that both latitude and longitude are within their valid ranges.\n *\n * @param latitude - Latitude value (-90 to 90)\n * @param longitude - Longitude value (-180 to 180)\n * @returns Object with `valid: true` if both are valid, or `valid: false` with an `error` message\n */\nexport function validateCoordinates(\n latitude: number,\n longitude: number,\n): { valid: boolean; error?: string } {\n if (!isValidLatitude(latitude)) {\n return {\n valid: false,\n error: `Invalid latitude: ${latitude}. Must be between -90 and 90.`,\n };\n }\n\n if (!isValidLongitude(longitude)) {\n return {\n valid: false,\n error: `Invalid longitude: ${longitude}. Must be between -180 and 180.`,\n };\n }\n\n return { valid: true };\n}\n","/**\n * Static Map Generation\n *\n * Generates static map URLs and fetches map images for location-based content.\n * Supports Mapbox and Google Maps providers.\n *\n * @example\n * ```typescript\n * import { getStaticMapUrl, fetchStaticMap } from '@happyvertical/geo';\n *\n * // Get URL for embedding\n * const url = getStaticMapUrl(53.5461, -113.4938, {\n * provider: 'mapbox',\n * zoom: 14,\n * width: 1200,\n * height: 630\n * });\n *\n * // Fetch the actual image\n * const buffer = await fetchStaticMap(53.5461, -113.4938, {\n * provider: 'mapbox'\n * });\n * await writeFile('map.png', buffer);\n * ```\n */\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Static map provider\n */\nexport type StaticMapProvider = 'mapbox' | 'google';\n\n/**\n * Mapbox map styles\n * @see https://docs.mapbox.com/api/maps/styles/\n */\nexport type MapboxStyle =\n | 'streets-v12'\n | 'outdoors-v12'\n | 'light-v11'\n | 'dark-v11'\n | 'satellite-v9'\n | 'satellite-streets-v12';\n\n/**\n * Google Maps map types\n * @see https://developers.google.com/maps/documentation/maps-static/start\n */\nexport type GoogleMapType = 'roadmap' | 'satellite' | 'terrain' | 'hybrid';\n\n/**\n * Marker definition for the map\n */\nexport interface StaticMapMarker {\n /** Latitude */\n latitude: number;\n /** Longitude */\n longitude: number;\n /** Marker color (hex or named color) */\n color?: string;\n /** Marker label (single character for Google, any for Mapbox) */\n label?: string;\n /** Marker size ('small' | 'medium' | 'large') */\n size?: 'small' | 'medium' | 'large';\n}\n\n/**\n * Options for static map generation\n */\nexport interface StaticMapOptions {\n /**\n * Map provider\n * @default 'mapbox'\n */\n provider?: StaticMapProvider;\n\n /**\n * Image width in pixels\n * @default 1200\n */\n width?: number;\n\n /**\n * Image height in pixels\n * @default 630\n */\n height?: number;\n\n /**\n * Zoom level (1-20)\n * @default 14\n */\n zoom?: number;\n\n /**\n * Marker color (hex without # or named color)\n * @default 'e74c3c' (red)\n */\n markerColor?: string;\n\n /**\n * Show a marker at the center point\n * @default true\n */\n showMarker?: boolean;\n\n /**\n * Mapbox access token (required for Mapbox provider)\n * Falls back to MAPBOX_ACCESS_TOKEN env var\n */\n mapboxToken?: string;\n\n /**\n * Google Maps API key (required for Google provider)\n * Falls back to GOOGLE_MAPS_API_KEY env var\n */\n googleApiKey?: string;\n\n /**\n * Mapbox style\n * @default 'streets-v12'\n */\n mapboxStyle?: MapboxStyle;\n\n /**\n * Google map type\n * @default 'roadmap'\n */\n googleMapType?: GoogleMapType;\n\n /**\n * Pixel density (1 or 2 for retina)\n * @default 1\n */\n scale?: 1 | 2;\n\n /**\n * Additional markers to display\n */\n markers?: StaticMapMarker[];\n}\n\n/**\n * Result from fetching a static map\n */\nexport interface StaticMapResult {\n /** PNG image buffer */\n buffer: Buffer;\n /** Image width */\n width: number;\n /** Image height */\n height: number;\n /** MIME type */\n mimeType: 'image/png';\n /** The URL that was fetched */\n url: string;\n}\n\n// ============================================================================\n// URL Generators\n// ============================================================================\n\n/**\n * Generate Mapbox static map URL\n */\nfunction getMapboxUrl(\n latitude: number,\n longitude: number,\n options: StaticMapOptions,\n): string {\n const token =\n options.mapboxToken ??\n process.env.MAPBOX_ACCESS_TOKEN ??\n process.env.MAPBOX_TOKEN;\n if (!token) {\n throw new Error(\n 'Mapbox access token required. Provide via options.mapboxToken or MAPBOX_ACCESS_TOKEN env var.',\n );\n }\n\n const width = options.width ?? 1200;\n const height = options.height ?? 630;\n const zoom = options.zoom ?? 14;\n const style = options.mapboxStyle ?? 'streets-v12';\n const scale = options.scale ?? 1;\n const showMarker = options.showMarker ?? true;\n\n // Build overlay for marker\n let overlay = '';\n if (showMarker) {\n const color = (options.markerColor ?? 'e74c3c').replace('#', '');\n // Mapbox marker format: pin-{size}-{label}+{color}({lon},{lat})\n overlay = `pin-l+${color}(${longitude},${latitude})/`;\n }\n\n // Add additional markers\n if (options.markers?.length) {\n const markerOverlays = options.markers.map((m) => {\n const color = (m.color ?? 'e74c3c').replace('#', '');\n const size = m.size === 'small' ? 's' : m.size === 'large' ? 'l' : 'm';\n const label = m.label ? `-${m.label}` : '';\n return `pin-${size}${label}+${color}(${m.longitude},${m.latitude})`;\n });\n overlay = markerOverlays.join(',') + '/';\n }\n\n const retinaStr = scale === 2 ? '@2x' : '';\n\n return `https://api.mapbox.com/styles/v1/mapbox/${style}/static/${overlay}${longitude},${latitude},${zoom}/${width}x${height}${retinaStr}?access_token=${token}`;\n}\n\n/**\n * Generate Google Maps static map URL\n */\nfunction getGoogleUrl(\n latitude: number,\n longitude: number,\n options: StaticMapOptions,\n): string {\n const apiKey =\n options.googleApiKey ??\n process.env.GOOGLE_MAPS_API_KEY ??\n process.env.GOOGLE_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'Google Maps API key required. Provide via options.googleApiKey or GOOGLE_MAPS_API_KEY env var.',\n );\n }\n\n const width = options.width ?? 1200;\n const height = options.height ?? 630;\n const zoom = options.zoom ?? 14;\n const mapType = options.googleMapType ?? 'roadmap';\n const scale = options.scale ?? 1;\n const showMarker = options.showMarker ?? true;\n\n const params = new URLSearchParams({\n center: `${latitude},${longitude}`,\n zoom: zoom.toString(),\n size: `${width}x${height}`,\n maptype: mapType,\n scale: scale.toString(),\n key: apiKey,\n });\n\n // Add center marker\n if (showMarker) {\n const color = (options.markerColor ?? 'red').replace('#', '0x');\n params.append('markers', `color:${color}|${latitude},${longitude}`);\n }\n\n // Add additional markers\n if (options.markers?.length) {\n for (const m of options.markers) {\n const color = (m.color ?? 'red').replace('#', '0x');\n const size = m.size ?? 'medium';\n const label = m.label ? `|label:${m.label.charAt(0).toUpperCase()}` : '';\n params.append(\n 'markers',\n `color:${color}|size:${size}${label}|${m.latitude},${m.longitude}`,\n );\n }\n }\n\n return `https://maps.googleapis.com/maps/api/staticmap?${params.toString()}`;\n}\n\n// ============================================================================\n// Main Functions\n// ============================================================================\n\n/**\n * Generate a static map URL\n *\n * Returns a URL that can be used directly in <img> tags or fetched for processing.\n *\n * @param latitude - Center latitude\n * @param longitude - Center longitude\n * @param options - Map options\n * @returns URL string for the static map\n *\n * @example Mapbox (default)\n * ```typescript\n * const url = getStaticMapUrl(53.5461, -113.4938, {\n * provider: 'mapbox',\n * zoom: 14,\n * mapboxStyle: 'streets-v12'\n * });\n * ```\n *\n * @example Google Maps\n * ```typescript\n * const url = getStaticMapUrl(53.5461, -113.4938, {\n * provider: 'google',\n * zoom: 14,\n * googleMapType: 'roadmap'\n * });\n * ```\n *\n * @example With custom markers\n * ```typescript\n * const url = getStaticMapUrl(53.5461, -113.4938, {\n * showMarker: false, // Hide center marker\n * markers: [\n * { latitude: 53.5461, longitude: -113.4938, color: 'blue', label: 'A' },\n * { latitude: 53.5500, longitude: -113.4900, color: 'green', label: 'B' }\n * ]\n * });\n * ```\n */\nexport function getStaticMapUrl(\n latitude: number,\n longitude: number,\n options: StaticMapOptions = {},\n): string {\n const provider = options.provider ?? 'mapbox';\n\n switch (provider) {\n case 'google':\n return getGoogleUrl(latitude, longitude, options);\n case 'mapbox':\n default:\n return getMapboxUrl(latitude, longitude, options);\n }\n}\n\n/**\n * Fetch a static map image\n *\n * Downloads the map image and returns it as a Buffer.\n *\n * @param latitude - Center latitude\n * @param longitude - Center longitude\n * @param options - Map options\n * @returns Promise resolving to the map image result\n *\n * @example\n * ```typescript\n * const result = await fetchStaticMap(53.5461, -113.4938, {\n * provider: 'mapbox',\n * width: 1200,\n * height: 630\n * });\n *\n * await fs.writeFile('map.png', result.buffer);\n * console.log(`Fetched ${result.width}x${result.height} map`);\n * ```\n */\nexport async function fetchStaticMap(\n latitude: number,\n longitude: number,\n options: StaticMapOptions = {},\n): Promise<StaticMapResult> {\n const width = options.width ?? 1200;\n const height = options.height ?? 630;\n const url = getStaticMapUrl(latitude, longitude, options);\n\n const response = await fetch(url);\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Failed to fetch static map: ${response.status} ${response.statusText} - ${errorText}`,\n );\n }\n\n const arrayBuffer = await response.arrayBuffer();\n const buffer = Buffer.from(arrayBuffer);\n\n return {\n buffer,\n width,\n height,\n mimeType: 'image/png',\n url,\n };\n}\n\n/**\n * Generate OG-sized static map URL (1200x630)\n *\n * Convenience function for generating Open Graph / social media sized maps.\n *\n * @param latitude - Center latitude\n * @param longitude - Center longitude\n * @param options - Map options (width/height ignored, set to 1200x630)\n * @returns URL string for the static map\n *\n * @example\n * ```typescript\n * const url = getOGMapUrl(53.5461, -113.4938);\n * // Returns 1200x630 map URL\n * ```\n */\nexport function getOGMapUrl(\n latitude: number,\n longitude: number,\n options: Omit<StaticMapOptions, 'width' | 'height'> = {},\n): string {\n return getStaticMapUrl(latitude, longitude, {\n ...options,\n width: 1200,\n height: 630,\n });\n}\n","/**\n * Geo package entry point\n * Provides standardized geographical information interface\n */\n\nimport { loadEnvConfig } from '@happyvertical/utils';\nimport type {\n GeoAdapter,\n GeoAdapterOptions,\n GoogleMapsOptions,\n OpenStreetMapOptions,\n} from './shared/types';\n\n// Export all types\nexport * from './shared/types';\nexport * from './shared/utils';\n\n// Export static map generation\nexport {\n fetchStaticMap,\n type GoogleMapType,\n getOGMapUrl,\n getStaticMapUrl,\n type MapboxStyle,\n type StaticMapMarker,\n type StaticMapOptions,\n type StaticMapProvider,\n type StaticMapResult,\n} from './static-maps';\n\n/**\n * Type guard for Google Maps options\n */\nfunction isGoogleMapsOptions(\n options: GeoAdapterOptions,\n): options is GoogleMapsOptions {\n return options.provider === 'google';\n}\n\n/**\n * Type guard for OpenStreetMap options\n */\nfunction isOpenStreetMapOptions(\n options: GeoAdapterOptions,\n): options is OpenStreetMapOptions {\n return options.provider === 'openstreetmap';\n}\n\n/**\n * Factory function to create a geo adapter instance\n *\n * Supports configuration via environment variables using the pattern:\n * - HAVE_GEO_PROVIDER → provider ('google' | 'openstreetmap')\n * - GOOGLE_MAPS_API_KEY → apiKey (for Google Maps provider)\n *\n * User-provided options always take precedence over environment variables.\n *\n * @param options - Configuration options for the geo provider (optional)\n * @returns Promise resolving to a geo adapter that implements GeoAdapter\n *\n * @example\n * ```typescript\n * // Create Google Maps adapter with explicit options\n * const googleGeo = await getGeoAdapter({\n * provider: 'google',\n * apiKey: process.env.GOOGLE_MAPS_API_KEY!\n * });\n *\n * // Create adapter using environment variables\n * // HAVE_GEO_PROVIDER=google\n * // GOOGLE_MAPS_API_KEY=your-api-key\n * const geoFromEnv = await getGeoAdapter();\n *\n * // Create OpenStreetMap adapter\n * const osmGeo = await getGeoAdapter({\n * provider: 'openstreetmap'\n * });\n *\n * // Use the adapter\n * const locations = await googleGeo.lookup('Eiffel Tower');\n * const coords = await osmGeo.reverseGeocode(48.8584, 2.2945);\n * ```\n */\nexport async function getGeoAdapter(\n options: Partial<GeoAdapterOptions> = {},\n): Promise<GeoAdapter> {\n // Load configuration from environment variables\n // Cast to any to handle union type with loadEnvConfig\n const config = loadEnvConfig(options as any, {\n packageName: 'geo',\n schema: {\n provider: 'string',\n timeout: 'number',\n maxResults: 'number',\n rateLimitDelay: 'number',\n userAgent: 'string',\n },\n }) as Partial<GeoAdapterOptions>;\n\n // Handle Google Maps API key from environment (not using HAVE_GEO_ prefix)\n if (config.provider === 'google' && !config.apiKey) {\n const apiKey = process.env.GOOGLE_MAPS_API_KEY;\n if (apiKey) {\n config.apiKey = apiKey;\n }\n }\n\n // Validate that we have a provider\n if (!config.provider) {\n throw new Error(\n 'Provider is required. Set via options.provider or HAVE_GEO_PROVIDER environment variable.',\n );\n }\n\n // Cast to full options type for provider-specific handling\n const fullOptions = config as GeoAdapterOptions;\n\n if (isGoogleMapsOptions(fullOptions)) {\n const { GoogleMapsProvider } = await import('./providers/google.js');\n return new GoogleMapsProvider(fullOptions);\n }\n\n if (isOpenStreetMapOptions(fullOptions)) {\n const { OpenStreetMapProvider } = await import(\n './providers/openstreetmap.js'\n );\n return new OpenStreetMapProvider(fullOptions);\n }\n\n // This should never happen due to TypeScript's discriminated union\n throw new Error(`Unsupported provider: ${(fullOptions as any).provider}`);\n}\n\n/** @internal */\nexport const PACKAGE_VERSION_INITIALIZED = true;\n"],"mappings":"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"}
|
package/metadata.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/geo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.80.1",
|
|
4
4
|
"description": "Standardized geographical information interface supporting Google Maps and OpenStreetMap",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -46,15 +46,15 @@
|
|
|
46
46
|
"license": "ISC",
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@googlemaps/google-maps-services-js": "^3.4.2",
|
|
49
|
-
"@happyvertical/cache": "0.
|
|
50
|
-
"@happyvertical/utils": "0.
|
|
49
|
+
"@happyvertical/cache": "0.80.1",
|
|
50
|
+
"@happyvertical/utils": "0.80.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/node": "25.0.10",
|
|
54
|
-
"typescript": "
|
|
55
|
-
"vite": "
|
|
54
|
+
"typescript": "5.9.3",
|
|
55
|
+
"vite": "8.1.4",
|
|
56
56
|
"vite-plugin-dts": "4.5.4",
|
|
57
|
-
"vitest": "
|
|
57
|
+
"vitest": "4.1.10"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"test": "vitest --config ../../vitest.package.config.ts run",
|
|
@@ -1,342 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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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"}
|