UNPKG

@rapideditor/location-conflation

Version:

Define complex geographic regions by including and excluding country codes and geojson shapes

10 lines 43.5 kB
{ "version": 3, "sources": ["../../src/location-conflation.ts"], "sourcesContent": [ "import * as CountryCoder from '@rapideditor/country-coder';\nimport * as Polyclip from 'polyclip-ts';\nimport calcArea from '@mapbox/geojson-area';\nimport circleToPolygon from 'circle-to-polygon';\nimport precision from 'geojson-precision';\nimport prettyStringify from 'json-stringify-pretty-compact';\nimport whichPolygon from 'which-polygon';\nimport type { Geom } from 'polyclip-ts';\n\nimport type {\n HasLocationSet,\n HasLocationSetID,\n LocoFeature,\n LocoProperties,\n Location,\n LocationID,\n LocationSet,\n LocationSetID,\n ResolvedLocation,\n ResolvedLocationSet,\n StringifyOptions,\n ValidatedLocation,\n ValidatedLocationSet,\n Vec2,\n} from './types.ts';\n\n\n// Re-export everything from types.ts so consumers can `import { … } from '@rapideditor/location-conflation'`\nexport type * from './types.ts';\n\n\n\n/**\n * LocationConflation lets you define complex geographic regions by including and\n * excluding country codes, points, and custom `.geojson` shapes.\n *\n * It resolves these definitions into GeoJSON `Feature`s with polygon geometries,\n * caching expensive polygon-clipping operations for performance.\n *\n * @example\n * ```ts\n * import { LocationConflation } from '@rapideditor/location-conflation';\n *\n * const loco = new LocationConflation(myFeatureCollection);\n *\n * const result = loco.resolveLocationSet({ include: ['de'], exclude: ['de-berlin.geojson'] });\n * console.log(result.feature); // GeoJSON Feature of Germany minus Berlin\n * ```\n */\nexport class LocationConflation {\n /**\n * Internal cache of all resolved features, keyed by stable identifiers.\n * Identifiers look like:\n * - `'[8.67039,49.41882]'` — point locations\n * - `'de-hamburg.geojson'` — geojson file locations\n * - `'Q2'` — country-coder locations (Wikidata QIDs)\n * - `'+[Q2]-[Q18,Q27611]'` — aggregated location sets\n */\n private _resolved: Map<LocationID | LocationSetID, LocoFeature>;\n\n // Internal cache of registered LocationSetID → approximate area in km²\n private _registered: Map<LocationSetID, number>;\n\n // Spatial index state, built by `registerLocationSets()` and rebuilt by `rebuildIndex()`.\n // Inverted index: locationID → Set of locationSetIDs that include/exclude that component location.\n // Reads as English: \"the sets including this location\" / \"the sets excluding this location\".\n private _setsIncluding: Map<LocationID, Set<LocationSetID>>;\n private _setsExcluding: Map<LocationID, Set<LocationSetID>>;\n\n // A `WhichPolygon` spatial index for custom .geojson and point-radius features only.\n // CountryCoder regions are looked up via CountryCoder's own spatial index.\n private _spatialIndex: ReturnType<typeof whichPolygon<{ id: string }>> | null;\n\n\n /**\n * @constructor\n * Creates a new LocationConflation instance.\n * @param fc - Optional GeoJSON FeatureCollection of known features with filename-like IDs\n * (e.g. `\"something.geojson\"`). Accepts a standard `GeoJSON.FeatureCollection` from\n * `@types/geojson` — no casts needed. Features without a `.geojson` id are silently skipped.\n */\n constructor(fc?: GeoJSON.FeatureCollection) {\n this._resolved = new Map();\n this._registered = new Map();\n this._setsIncluding = new Map();\n this._setsExcluding = new Map();\n this._spatialIndex = null;\n\n // Load any .geojson features from the input FeatureCollection.\n if (fc) {\n this.addFeatures(fc);\n }\n\n this._setupWorld(); // will also call rebuildIndex()\n }\n\n\n /**\n * Setup the world objects.\n * This ensures that we both have our world location and world locationSet available.\n *\n * Calling `registerLocationSets` is important here because allows `locationSetsAt`\n * to return the world locationSet, even if no locationSets have been otherwise registered.\n */\n private _setupWorld(): void {\n // Note that CountryCoder's \"world\" is an aggregate of its administrative boundaries.\n // (It excludes regions like the north pole or ocean.)\n // What we want for our purposes is a polygon that covers the entire world.\n const WORLD_GEOMETRY: GeoJSON.Polygon = {\n type: 'Polygon',\n coordinates: [[[-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90]]],\n };\n\n // 'Q2' - A Location representing the entire world.\n const WORLD_LOCATION: LocoFeature = {\n type: 'Feature',\n id: 'Q2',\n properties: { id: 'Q2', area: _getArea(WORLD_GEOMETRY) },\n geometry: WORLD_GEOMETRY\n };\n\n // '+[Q2]' - A LocationSet that just includes the entire world.\n const WORLD_LOCATIONSET: HasLocationSet = {\n locationSet: { include: ['Q2'] }\n };\n\n this._resolved.set('Q2', WORLD_LOCATION);\n this.registerLocationSets([WORLD_LOCATIONSET]); // will also call rebuildIndex()\n }\n\n\n /**\n * Returns whether a coordinate pair is within valid WGS84 bounds.\n */\n private _isValidPoint(loc: Vec2): boolean {\n const [lon, lat] = loc;\n return (\n Number.isFinite(lon) && lon >= -180 && lon <= 180 &&\n Number.isFinite(lat) && lat >= -90 && lat <= 90\n );\n }\n\n\n /**\n * Adds `.geojson` features to the cache of known locations.\n *\n * Each feature must have a filename-like `id` ending in `.geojson` (either on\n * the feature itself or in its `properties`). Features without a qualifying\n * `id` are silently skipped.\n *\n * The method normalizes each feature into the {@link LocoFeature} shape\n * (lowercased id, guaranteed `area` property, polygon geometry). If a\n * feature with the same id already exists in the cache, it is replaced.\n *\n * @param fc - A GeoJSON FeatureCollection containing custom locations.\n * Accepts a standard `GeoJSON.FeatureCollection` — no casts needed.\n *\n * @example\n * ```ts\n * // Load additional features after construction\n * const loco = new LocationConflation();\n * loco.addFeatures(additionalFeatureCollection);\n *\n * // Now these features are available for resolving\n * const result = loco.resolveLocation('philly_metro.geojson');\n * ```\n */\n addFeatures(fc: GeoJSON.FeatureCollection): void {\n if (fc?.type !== 'FeatureCollection' || !Array.isArray(fc.features)) return;\n\n for (const feature of fc.features) {\n const props = feature.properties ?? {};\n\n // Get `id` from either `id` or `properties.id`.\n // Features without a valid id are silently skipped.\n let id = feature.id ?? props['id'];\n if (!id || !/^\\S+\\.geojson$/i.test(String(id))) continue;\n\n // Ensure `id` exists and is lowercase\n id = String(id).toLowerCase();\n\n const geometry = feature.geometry as GeoJSON.Polygon | GeoJSON.MultiPolygon;\n\n // Ensure `area` property exists.\n // The purpose of the `area` property is to enable rough size comparisons.\n const existingArea = props['area'] as number | undefined;\n const area = existingArea || _getArea(geometry);\n\n const lcFeature: LocoFeature = {\n type: 'Feature',\n id,\n properties: { ...props, id, area } as LocoProperties,\n geometry,\n };\n\n this._resolved.set(id, lcFeature);\n }\n\n this.rebuildIndex(); // Need to rebuild the index after adding new locations.\n }\n\n\n /**\n * Removes `.geojson` features from the cache by id.\n * Each id should be a filename-like string ending in `.geojson`.\n * Ids are matched case-insensitively (lowercased before lookup).\n * Non-`.geojson` ids and ids not present in the cache are silently ignored.\n *\n * @param ids - One or more feature ids to remove.\n *\n * @example\n * ```ts\n * loco.removeFeatures('philly_metro.geojson', 'dc_metro.geojson');\n * ```\n */\n removeFeatures(...ids: string[]): void {\n for (const id of ids) {\n if (!/^\\S+\\.geojson$/i.test(id)) continue;\n this._resolved.delete(id.toLowerCase());\n }\n\n this.rebuildIndex(); // Need to rebuild the index after removing locations.\n }\n\n\n /**\n * Removes all cached resolved features.\n * This clears everything from the resolved cache — `.geojson` features, resolved\n * country-coder features, point circles, and aggregated location sets.\n * The world feature ('Q2') is re-added automatically.\n *\n * @example\n * ```ts\n * loco.clearFeatures();\n * // Cache now contains only the world feature (Q2)\n * ```\n */\n clearFeatures(): void {\n this._resolved.clear();\n this._setupWorld(); // will also call rebuildIndex()\n }\n\n\n /**\n * Backward-compatibility alias to direct access the internal resolved-feature cache.\n * If you use this to change the features, make sure to call `rebuildIndex()` after.\n * @deprecated Use the LocationConflation cache management APIs instead (`addFeatures`, `removeFeatures`, `clearFeatures`).\n */\n public get _cache(): Map<string, LocoFeature> {\n return this._resolved;\n }\n\n\n /**\n * Validates a location and returns its type and stable identifier.\n * @param location - Location to validate (point, geojson filename, or country code)\n * @returns Validated location result object\n * @throws Throws Error if the given location is invalid.\n *\n * @example\n * ```typescript\n * // Point location with default 25km radius\n * loco.validateLocation([8.67039, 49.41882]);\n * // => { type: 'point', location: [8.67039, 49.41882], id: '[8.67039,49.41882]' }\n *\n * // Point location with custom radius\n * loco.validateLocation([-77.0369, 38.9072, 10]);\n * // => { type: 'point', location: [-77.0369, 38.9072, 10], id: '[-77.0369,38.9072,10]' }\n *\n * // Country code\n * loco.validateLocation('de');\n * // => { type: 'countrycoder', location: 'de', id: 'Q183' }\n * loco.validateLocation('001');\n * // => { type: 'countrycoder', location: '001', id: 'Q2' }\n *\n * // GeoJSON file\n * loco.validateLocation('philly_metro.geojson');\n * // => { type: 'geojson', location: 'philly_metro.geojson', id: 'philly_metro.geojson' }\n * ```\n */\n validateLocation(location: Location): ValidatedLocation {\n // A [lon, lat] or [lon, lat, radius] point?\n if (Array.isArray(location) && (location.length === 2 || location.length === 3)) {\n const lon = location[0];\n const lat = location[1];\n const radius = location[2];\n if (\n this._isValidPoint([lon, lat]) &&\n (location.length === 2 || (radius !== undefined && Number.isFinite(radius) && radius > 0))\n ) {\n const id = '[' + location.toString() + ']';\n return { type: 'point', location, id };\n }\n\n // A custom .geojson filename?\n } else if (typeof location === 'string' && /^\\S+\\.geojson$/i.test(location)) {\n const id = location.toLowerCase();\n if (this._resolved.has(id)) {\n return { type: 'geojson', location, id };\n }\n\n // A country-coder identifier?\n } else if (typeof location === 'string' || typeof location === 'number') {\n // a country-coder value?\n const feature = CountryCoder.feature(location);\n if (feature) {\n // Normalize CountryCoder location id by using the returned Wikidata QID.\n // It's the one property that everything in CountryCoder is guaranteed to have,\n // and it allows us to match 'Q2' World and resolve it to our own world polygon.\n const id = feature.properties.wikidata;\n return { type: 'countrycoder', location, id };\n }\n }\n\n throw new Error(`validateLocation: Invalid location: \"${location}\".`);\n }\n\n\n /**\n * Resolves a location to a GeoJSON Feature.\n * @param location - Location to resolve\n * @returns Resolved location result object, including the GeoJSON Feature\n * @throws Throws Error if the given location is invalid or cannot be resolved.\n *\n * @example\n * ```typescript\n * // Resolve a point location to a circular polygon\n * const result = loco.resolveLocation([8.67039, 49.41882]);\n * // result.feature is a GeoJSON Feature with a circular Polygon geometry\n *\n * // Resolve a country code\n * const germany = loco.resolveLocation('de');\n * // germany.feature is a GeoJSON Feature with Germany's boundary\n *\n * // Resolve a custom GeoJSON file\n * const metro = loco.resolveLocation('philly_metro.geojson');\n * // metro.feature is the pre-loaded GeoJSON Feature\n * ```\n */\n resolveLocation(location: Location): ResolvedLocation {\n const valid = this.validateLocation(location);\n const id = valid.id;\n\n // Return an already-resolved result from cache if we can.\n // This will hit cache for:\n // - all custom .geojson\n // - previously generated point-radius\n // - previously seen country coder locations\n // - our custom 'Q2' World location\n if (this._resolved.has(id)) {\n return { ...valid, feature: this._resolved.get(id)! };\n }\n\n // A newly seen [lon, lat] or [lon, lat, radius] point?\n if (valid.type === 'point' && Array.isArray(location)) {\n const lon = location[0];\n const lat = location[1];\n const radius = location[2] || 25; // km\n const EDGES = 10;\n const PRECISION = 3;\n const area = Math.PI * radius * radius; // km²\n const feature: LocoFeature = precision(\n {\n type: 'Feature',\n id,\n properties: { id, area: Number(area.toFixed(2)) },\n geometry: circleToPolygon([lon, lat], radius * 1000, EDGES), // km to m\n },\n PRECISION\n );\n this._resolved.set(id, feature);\n return { ...valid, feature };\n }\n\n // A newly seen country-coder identifier?\n if (valid.type === 'countrycoder') {\n // id is a wikidata QID that validateLocation already confirmed exists in CountryCoder\n const ccFeature = CountryCoder.feature(id)!;\n const ccProps = ccFeature.properties;\n let geometry: GeoJSON.Polygon | GeoJSON.MultiPolygon | undefined;\n\n // -> This block of code is weird and requires some explanation. <-\n // CountryCoder includes higher level features which are made up of members.\n // These features don't have their own geometry, but CountryCoder provides an\n // `aggregateFeature` method to combine these members into a MultiPolygon.\n // In the past, Turf/JSTS/martinez could not handle the aggregated features,\n // so we'd iteratively union them all together. (this was slow)\n // But now mfogel/polygon-clipping handles these MultiPolygons like a boss.\n // This approach also has the benefit of removing all the internal borders and\n // simplifying the regional polygons a lot.\n if (Array.isArray(ccProps.members)) {\n const aggregate = CountryCoder.aggregateFeature(id);\n if (aggregate) {\n const clipped = _clip([{\n type: 'Feature',\n id: '',\n properties: {} as LocoProperties,\n geometry: aggregate.geometry as GeoJSON.Polygon | GeoJSON.MultiPolygon,\n }], 'UNION');\n if (clipped) {\n geometry = clipped.geometry;\n }\n }\n }\n\n // Clone geometry so we never hold a reference to CountryCoder's internal data.\n // Skip the clone for aggregate features — clip() already produced a fresh geometry.\n if (!geometry) {\n geometry = structuredClone(ccFeature.geometry) as GeoJSON.Polygon | GeoJSON.MultiPolygon;\n }\n\n // Ensure `area` property exists\n const area = _getArea(geometry);\n\n const feature: LocoFeature = {\n type: 'Feature',\n id,\n properties: { id, area } as LocoProperties,\n geometry,\n };\n\n this._resolved.set(id, feature);\n return { ...valid, feature };\n }\n\n throw new Error(`resolveLocation: Couldn't resolve location \"${location}\".`);\n }\n\n\n /**\n * Validates a locationSet and returns its stable identifier.\n * @param locationSet - LocationSet with include/exclude arrays\n * @returns Validated locationSet result object\n * @throws Throws Error if any referenced location is invalid, or locationSet has no include.\n *\n * @example\n * ```typescript\n * // Include multiple countries\n * loco.validateLocationSet({ include: ['de', 'fr', 'it'] });\n * // => { type: 'locationset', locationSet: {...}, id: '+[Q183,Q142,Q38]' }\n *\n * // Include with exclusions\n * loco.validateLocationSet({\n * include: ['de'],\n * exclude: ['de-berlin.geojson']\n * });\n * // => { type: 'locationset', locationSet: {...}, id: '+[Q183]-[de-berlin.geojson]' }\n *\n * // Mix different location types\n * loco.validateLocationSet({\n * include: ['us', [8.67039, 49.41882], 'philly_metro.geojson']\n * });\n * ```\n */\n validateLocationSet(locationSet?: LocationSet): ValidatedLocationSet {\n locationSet = locationSet || {};\n const validator = this.validateLocation.bind(this);\n const include = (locationSet.include || []).map(validator) as ValidatedLocation[];\n const exclude = (locationSet.exclude || []).map(validator) as ValidatedLocation[];\n\n if (!include.length) {\n throw new Error('validateLocationSet: LocationSet includes nothing.');\n }\n\n // Generate stable identifier\n include.sort(_sortLocations);\n let id = '+[' + include.map((d) => d.id).join(',') + ']';\n if (exclude.length) {\n exclude.sort(_sortLocations);\n id += '-[' + exclude.map((d) => d.id).join(',') + ']';\n }\n\n return { type: 'locationset', locationSet, id };\n }\n\n\n /**\n * Resolves a locationSet to a GeoJSON Feature by combining included/excluded regions.\n * @param locationSet - LocationSet with include/exclude arrays\n * @returns Resolved locationSet result object, including the GeoJSON Feature\n * @throws Throws Error if any referenced location is invalid, or locationSet has no include.\n *\n * @example\n * ```typescript\n * // Combine multiple countries into one feature\n * const benelux = loco.resolveLocationSet({\n * include: ['be', 'nl', 'lu']\n * });\n * // benelux.feature is a GeoJSON Feature with combined boundaries\n *\n * // Germany excluding Berlin\n * const germanyNoCapital = loco.resolveLocationSet({\n * include: ['de'],\n * exclude: ['de-berlin.geojson']\n * });\n * // Result is Germany with Berlin cut out\n *\n * // Complex region definition\n * const customRegion = loco.resolveLocationSet({\n * include: ['us-ca', 'us-or', 'us-wa'],\n * exclude: [[8.67039, 49.41882, 50]]\n * });\n * // West coast states minus a 50km circle\n * ```\n */\n resolveLocationSet(locationSet?: LocationSet): ResolvedLocationSet {\n locationSet = locationSet || {};\n const valid = this.validateLocationSet(locationSet);\n const id = valid.id;\n\n // Return a result from cache if we can\n if (this._resolved.has(id)) {\n return { ...valid, feature: this._resolved.get(id)! };\n }\n\n const resolver = this.resolveLocation.bind(this);\n const includes = (locationSet.include || []).map(resolver) as ResolvedLocation[];\n const excludes = (locationSet.exclude || []).map(resolver) as ResolvedLocation[];\n\n // Return quickly if it's just a single included location.\n if (includes.length === 1 && excludes.length === 0) {\n return { ...valid, feature: includes[0]!.feature };\n }\n\n // Calculate unions\n const includeGeoJSON = _clip(includes.map((d) => d.feature), 'UNION')!;\n const excludeGeoJSON = _clip(excludes.map((d) => d.feature), 'UNION');\n\n // Calculate difference, update `area` and return result\n const resultGeoJSON = excludeGeoJSON ? _clip([includeGeoJSON, excludeGeoJSON], 'DIFFERENCE')! : includeGeoJSON;\n resultGeoJSON.id = id;\n resultGeoJSON.properties = { id, area: _getArea(resultGeoJSON.geometry) };\n\n this._resolved.set(id, resultGeoJSON);\n return { ...valid, feature: resultGeoJSON };\n }\n\n\n /**\n * Registers `locationSet`-bearing objects so that {@link locationSetsAt} can tell you\n * which of them cover a given point.\n *\n * For each object, this method:\n * 1. Validates the `locationSet` and assigns its stable `locationSetID` (e.g. `'+[Q183]'`).\n * 2. Walks the `include` / `exclude` components and records them in an inverted index\n * (component `locationID` → set of `locationSetID`s that reference it).\n * 3. Sums the approximate areas of the `include` components so results can be ranked\n * smallest-first.\n *\n * Unlike the single-item `validate*` / `resolve*` methods, this method is **tolerant** of\n * bad input so that a batch of thousands of presets won't be rejected over a single typo:\n * - Objects with a missing, empty, or invalid `locationSet` fall back to world (`+[Q2]`).\n * - Individual invalid `include` / `exclude` components are silently ignored.\n *\n * This method **accumulates** — calling it multiple times (e.g. for different data sources)\n * inserts all locationSets into the same index. The spatial index is rebuilt automatically.\n * To reset the index entirely, create a new `LocationConflation` instance.\n *\n * Note: this does **not** resolve each locationSet to a combined polygon — that expensive\n * clipping step is deferred to {@link resolveLocationSet}, called only when the caller\n * actually needs the GeoJSON geometry.\n *\n * @param objects - Objects with a `locationSet` property to index.\n * @returns The same array of objects, narrowed to guarantee `locationSetID` is set.\n *\n * @example\n * ```ts\n * const presets = [\n * { id: 'amenity/cafe', locationSet: { include: ['de'] } },\n * { id: 'amenity/atm', locationSet: { include: ['001'] } },\n * ];\n * loco.registerLocationSets(presets);\n * // presets[0].locationSetID === '+[Q183]'\n * // presets[1].locationSetID === '+[Q2]'\n * ```\n */\n registerLocationSets<T extends HasLocationSet>(objects: T[]): (T & HasLocationSetID)[] {\n if (!Array.isArray(objects)) return [];\n\n for (const obj of objects) {\n // Resolve the locationSet to a stable id; fall back to world on any problem\n // (missing, empty include, or all-invalid components).\n let locationSet: LocationSet = obj.locationSet ?? {};\n let locationSetID: LocationSetID;\n try {\n locationSetID = this.validateLocationSet(locationSet).id;\n } catch {\n locationSet = { include: ['Q2'] };\n locationSetID = '+[Q2]';\n }\n obj.locationSet = locationSet;\n obj.locationSetID = locationSetID;\n\n // If we've already registered this exact locationSetID, skip the inner work.\n // The object has already gotten its locationSetID assigned above.\n if (this._registered.has(locationSetID)) continue;\n\n let area = 0;\n\n for (const location of (locationSet.include ?? [])) {\n // Silently skip invalid/unresolvable locations in batch mode.\n let resolved: ResolvedLocation;\n try {\n resolved = this.resolveLocation(location);\n } catch {\n continue;\n }\n const locationID: LocationID = resolved.id;\n area += resolved.feature.properties.area;\n\n let s = this._setsIncluding.get(locationID);\n if (!s) { s = new Set(); this._setsIncluding.set(locationID, s); }\n s.add(locationSetID);\n }\n\n for (const location of (locationSet.exclude ?? [])) {\n // Silently skip invalid/unresolvable locations in batch mode.\n let resolved: ResolvedLocation;\n try {\n resolved = this.resolveLocation(location);\n } catch {\n continue;\n }\n const locationID: LocationID = resolved.id;\n\n let s = this._setsExcluding.get(locationID);\n if (!s) { s = new Set(); this._setsExcluding.set(locationID, s); }\n s.add(locationSetID);\n }\n\n this._registered.set(locationSetID, area);\n }\n\n this.rebuildIndex();\n\n return objects as unknown as (T & HasLocationSetID)[];\n }\n\n\n /**\n * Rebuilds the internal spatial index from the current cache and inverted index.\n *\n * Normally you don't need to call this yourself — {@link registerLocationSets},\n * {@link addFeatures}, {@link removeFeatures}, and {@link clearFeatures} all rebuild\n * the index automatically when appropriate. Call it manually only if you've mutated\n * the cache through the (deprecated) `_cache` getter.\n */\n rebuildIndex(): void {\n // Collect all unique component locationIDs referenced by the inverted index.\n const allLocationIDs = new Set([\n ...this._setsIncluding.keys(),\n ...this._setsExcluding.keys(),\n ]);\n\n // Only custom .geojson features (ids ending in `.geojson`) and point-radius features\n // (ids starting with `[`) go into our which-polygon index. CountryCoder regions are\n // deliberately skipped here because CountryCoder already provides its own optimized\n // spatial index via `featuresContaining()`, which we use directly in `locationSetsAt`.\n const features: Array<{ type: 'Feature'; geometry: GeoJSON.Polygon | GeoJSON.MultiPolygon; properties: { id: LocationID } }> = [];\n for (const locationID of allLocationIDs) {\n if (!locationID.startsWith('[') && !/\\.geojson$/i.test(locationID)) continue;\n\n const feature = this._resolved.get(locationID);\n if (feature) {\n features.push({\n type: 'Feature',\n geometry: feature.geometry,\n properties: { id: locationID },\n });\n }\n }\n\n this._spatialIndex = whichPolygon({ type: 'FeatureCollection', features });\n }\n\n\n /**\n * Returns the registered locationSets that cover the given point,\n * mapped to their approximate area in km².\n *\n * Uses the inverted spatial index built by {@link registerLocationSets} — no polygon clipping is\n * performed. For country-coder regions, CountryCoder's built-in spatial index is used.\n * For custom `.geojson` and point-radius features, our local which-polygon index is used.\n *\n * The returned `Map` gives callers O(1) `has(locationSetID)` membership tests (the common\n * \"is this locationSet valid here?\" check).\n * Results are not sorted — if you need them ordered, sort `[...result.entries()]` by value.\n *\n * Call {@link resolveLocationSet} on any returned ID if you need the actual GeoJSON feature.\n *\n * @param loc - `[longitude, latitude]` coordinate to query.\n * @returns Map of locationSetID → approximate area (km²), for all sets valid at the point.\n *\n * @example\n * ```ts\n * loco.registerLocationSets(presets);\n * const hits = loco.locationSetsAt([-75.16, 39.95]);\n * if (hits.has('+[Q30]')) { ... }\n * // Iterate smallest-first:\n * const sorted = [...hits.entries()].sort((a, b) => a[1] - b[1]);\n * ```\n */\n locationSetsAt(loc: Vec2): Map<LocationSetID, number> {\n const results = new Map<LocationSetID, number>();\n const isValidPoint = this._isValidPoint(loc);\n\n // Two-pass design:\n // 1. For each component location covering the point, walk the inverted index to\n // collect the locationSetIDs that include or exclude it.\n // 2. Apply exclusions (a locationSet that excludes any covering location is removed).\n //\n // Location lookup is split across two spatial indexes because CountryCoder regions\n // use CountryCoder's own index, while custom .geojson and point-radius features use ours.\n // Unlike some earlier designs, we do NOT put resolved locationSet polygons into\n // which-polygon — membership is always derived from location hits via the inverted\n // index. This keeps the spatial index small and avoids needing to resolve every\n // locationSet to a GeoJSON polygon, which is expensive.\n const toExclude = new Set<LocationSetID>();\n\n // Search the inverted index for occurrances of the given locationID.\n const gather = (locationID: LocationID): void => {\n for (const id of (this._setsIncluding.get(locationID) ?? [])) {\n results.set(id, this._registered.get(id) ?? Infinity);\n }\n for (const id of (this._setsExcluding.get(locationID) ?? [])) {\n toExclude.add(id);\n }\n };\n\n // Country-coder components: delegate to CountryCoder's own spatial index.\n // CountryCoder models \"world\" as an aggregation of known administrative features,\n // which can be empty in places like open ocean/poles. We still guarantee `+[Q2]`\n // for any valid coordinate via special handling below.\n if (isValidPoint) {\n try {\n for (const ccFeature of CountryCoder.featuresContaining(loc)) {\n gather(ccFeature.properties.wikidata);\n }\n } catch {\n // Defensive: treat lookup failures as \"no CountryCoder matches\".\n }\n }\n\n // Custom .geojson and point-radius components: use our which-polygon index\n // which-polygon returns null (not []) when multi=true and no matches\n if (isValidPoint && this._spatialIndex) {\n for (const props of (this._spatialIndex(loc, true) ?? [])) {\n gather(props.id);\n }\n }\n\n // Apply exclusions\n for (const id of toExclude) {\n results.delete(id);\n }\n\n // Our world means \"any valid lon/lat in [-180..180] x [-90..90]\", not just\n // CountryCoder's aggregated administrative coverage. Ensure `+[Q2]` is present\n // for valid coordinates unless that locationSet was explicitly excluded.\n if (isValidPoint && !toExclude.has('+[Q2]')) {\n const worldArea = this._registered.get('+[Q2]');\n if (worldArea !== undefined) {\n results.set('+[Q2]', worldArea);\n }\n }\n\n return results;\n }\n\n /**\n * Returns the approximate area (in km²) of a registered locationSet.\n * The area is the sum of `include` location areas computed during\n * {@link registerLocationSets} — it does not subtract `exclude` areas.\n * Returns `undefined` if the locationSet has not been registered.\n *\n * @param locationSetID - Stable locationSet identifier, e.g. `'+[Q183]'`.\n * @returns Approximate area in km², or `undefined` if not registered.\n *\n * @example\n * ```ts\n * loco.registerLocationSets([{ locationSet: { include: ['de'] } }]);\n * loco.getLocationSetArea('+[Q183]'); // ~357000\n * ```\n */\n getLocationSetArea(locationSetID: LocationSetID): number | undefined {\n return this._registered.get(locationSetID);\n }\n\n /**\n * Convenience method to pretty-stringify an object.\n * @param obj - Object to stringify\n * @param options - Stringify options\n * @returns Pretty-formatted JSON string\n *\n * @example\n * ```typescript\n * const result = loco.resolveLocation('de');\n * console.log(LocationConflation.stringify(result.feature));\n * // Outputs a compact, readable JSON representation of the feature\n *\n * // Custom formatting options\n * console.log(LocationConflation.stringify(result.feature, { indent: 2, maxLength: 80 }));\n * ```\n */\n static stringify(obj: unknown, options?: StringifyOptions): string {\n return prettyStringify(obj, options);\n }\n}\n\n\n/** The two polygon clipping operations used internally. */\ntype ClipOperation = 'UNION' | 'DIFFERENCE';\n\n/**\n * Wraps the polyclip-ts library and returns a GeoJSON feature.\n * @param features - Array of features to clip\n * @param which - Operation type (UNION or DIFFERENCE)\n * @returns Clipped GeoJSON feature or null if features array is empty\n */\nfunction _clip(features: LocoFeature[], which: ClipOperation): LocoFeature | null {\n if (!Array.isArray(features) || !features.length) return null;\n\n const fn = which === 'UNION' ? Polyclip.union : Polyclip.difference;\n const first = features[0]!.geometry.coordinates as Geom;\n const rest: Geom[] = [];\n for (let i = 1; i < features.length; i++) {\n rest.push(features[i]!.geometry.coordinates as Geom);\n }\n const coords = fn(first, ...rest);\n\n return {\n type: 'Feature',\n properties: {} as LocoProperties,\n geometry: {\n type: whichType(coords),\n coordinates: coords,\n } as GeoJSON.Polygon | GeoJSON.MultiPolygon,\n id: '',\n };\n\n // is this a Polygon or a MultiPolygon?\n function whichType(coords: unknown): 'Polygon' | 'MultiPolygon' {\n const a = Array.isArray(coords);\n const b = a && Array.isArray(coords[0]);\n const c = b && Array.isArray(coords[0][0]);\n const d = c && Array.isArray(coords[0][0][0]);\n return d ? 'MultiPolygon' : 'Polygon';\n }\n}\n\n\n/**\n * Sorting function for locations to generate deterministic IDs.\n * Note that sorting the include/exclude lists is ok because their contents end up unioned together.\n * @param a - First location\n * @param b - Second location\n * @returns Sort order\n */\nfunction _sortLocations(a: ValidatedLocation, b: ValidatedLocation): number {\n const rank = { countrycoder: 1, geojson: 2, point: 3 };\n const aRank = rank[a.type];\n const bRank = rank[b.type];\n\n return aRank > bRank ? 1 : aRank < bRank ? -1 : a.id.localeCompare(b.id);\n}\n\n\n/**\n * Compute the rough area of a GeoJSON geometry, in km².\n * We ensure that all resolved GeoJSON features have an `area` property.\n * The purpose of the `area` property is to enable rough size comparisons.\n * @param geom - The geometry to compute the area for\n * @returns Area, in km², and truncated to 2 decimal places\n */\nfunction _getArea(geom: GeoJSON.Geometry): number {\n return Number((calcArea.geometry(geom) / 1e6).toFixed(2)); // m² to km²\n}\n\n\nexport default LocationConflation;\n" ], "mappings": "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", "debugId": "7912B099A0052E7564756E2164756E21", "names": [] }