name-suggestion-index
Version:
Canonical common brand names for OpenStreetMap
12 lines • 64.6 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../../lib/matcher.ts", "../../lib/simplify.ts", "../../lib/stemmer.ts", "../../lib/sort_object.ts", "../../lib/presets_id.ts", "../../lib/presets_josm.ts"],
"sourcesContent": [
"import { LocationConflation } from '@rapideditor/location-conflation';\nimport { simplify } from './simplify.ts';\n\nimport type { HasLocationSet, HasLocationSetID, LocationSetID, Vec2 } from '@rapideditor/location-conflation';\nimport type { MatchHit, MatchIndexBranch, NsiData, NsiMatchGroupsJSON, NsiPath, NsiTree, NsiTreeProperties } from './types.ts';\n\n// Imported JSON (will be inlined by bun)\nimport matchGroupsJSON from '../config/matchGroups.json' with {type: 'json'};\nimport genericWordsJSON from '../config/genericWords.json' with {type: 'json'};\nimport treesJSON from '../config/trees.json' with {type: 'json'};\n\n/** Match-group definitions keyed by group name. */\nconst matchGroups: NsiMatchGroupsJSON['matchGroups'] = matchGroupsJSON.matchGroups;\n\n/** Tree configuration keyed by tree name (e.g. `brands`, `operators`). */\nconst trees: Record<NsiTree, NsiTreeProperties> = treesJSON.trees;\n\n\n// This is an unfortunate TypeScript-ism, essentially it does not consider a class instance\n// with private or protected members to be type-compatible with itself.\n// The workaround is to define an interface type so that instead of saying\n// \"buildLocationIndex accepts a LocationConflation instance\"\n// we need to say\n// \"buildLocationIndex accepts something that looks like a LocationConflation instance\"\n// @see https://www.reddit.com/r/typescript/comments/wv3d5m/private_properties_different_versions_of_same/\n// @see https://github.com/microsoft/TypeScript/issues/18499 (and related)\n/** LocationConflation _structural_ type - see name-suggestion-index#12150 **/\nexport interface LocationResolver {\n registerLocationSets<T extends HasLocationSet>(objects: T[]): (T & HasLocationSetID)[];\n locationSetsAt(loc: Vec2): Map<LocationSetID, number>;\n getLocationSetArea(locationSetID: LocationSetID): number | undefined;\n}\n\n\n/**\n * Matches OpenStreetMap `[key, value, name]` tuples against the\n * Name Suggestion Index (NSI) canonical items.\n *\n * Typical usage:\n * ```ts\n * const matcher = new Matcher();\n * matcher.buildMatchIndex(data);\n * matcher.buildLocationIndex(data, loco); // optional\n * const hits = matcher.match('amenity', 'bank', 'Wells Fargo', [-122.4, 37.8]);\n * ```\n */\nexport class Matcher {\n /** Primary match index: `kv → { primary, alternate, excludeGeneric, excludeNamed }`. */\n private matchIndex: Map<string, MatchIndexBranch> | undefined;\n /** Map of generic-word pattern strings to compiled RegExp objects. */\n private genericWords = new Map<string, RegExp>();\n /** The location resolver used to resolve locationSets (set by {@link buildLocationIndex}). */\n private loco: LocationResolver | undefined;\n /** Map of item id → locationSetID, populated by {@link buildLocationIndex}. */\n private itemLocationSetID: Map<string, LocationSetID> | undefined;\n /** Warnings collected during index building (e.g. duplicate cache keys). */\n private warnings: Array<string> = [];\n\n\n /**\n * Creates a new Matcher and initialises the generic-word regex table\n * from `config/genericWords.json`.\n */\n constructor() {\n // The `matchIndex` is a specialized structure that allows us to quickly answer\n // _\"Given a [key/value tagpair, name, location], what canonical items (brands etc) can match it?\"_\n //\n // The index contains all valid combinations of k/v tagpairs and names\n // matchIndex:\n // {\n // 'k/v': {\n // 'primary': Map (String 'nsimple' -> Set (itemIDs…), // matches for tags like `name`, `name:xx`, etc.\n // 'alternate': Map (String 'nsimple' -> Set (itemIDs…), // matches for tags like `alt_name`, `brand`, etc.\n // 'excludeNamed': Map (String 'pattern' -> RegExp),\n // 'excludeGeneric': Map (String 'pattern' -> RegExp)\n // },\n // }\n //\n // {\n // 'amenity/bank': {\n // 'primary': {\n // 'firstbank': Set (\"firstbank-978cca\", \"firstbank-9794e6\", \"firstbank-f17495\", …),\n // …\n // },\n // 'alternate': {\n // '1stbank': Set (\"firstbank-f17495\"),\n // …\n // }\n // },\n // 'shop/supermarket': {\n // 'primary': {\n // 'coop': Set (\"coop-76454b\", \"coop-ebf2d9\", \"coop-36e991\", …),\n // 'coopfood': Set (\"coopfood-a8278b\", …),\n // …\n // },\n // 'alternate': {\n // 'coop': Set (\"coopfood-a8278b\", …),\n // 'federatedcooperatives': Set (\"coop-76454b\", …),\n // 'thecooperative': Set (\"coopfood-a8278b\", …),\n // …\n // }\n // }\n // }\n //\n this.matchIndex = undefined;\n\n // The `genericWords` structure matches the contents of genericWords.json to instantiated RegExp objects\n // Map (String 'pattern' -> RegExp),\n this.genericWords = new Map();\n for (const s of (genericWordsJSON.genericWords || [])) {\n this.genericWords.set(s, new RegExp(s, 'i'));\n }\n\n // A reference to the `LocationConflation` instance supplied to `buildLocationIndex`.\n // At match time we call `loco.locationSetsAt(point)` and `loco.getLocationSetArea(id)`\n // instead of maintaining our own parallel indexes.\n this.loco = undefined;\n this.itemLocationSetID = undefined;\n\n // Array of match conflict pairs (currently unused)\n this.warnings = [];\n }\n\n\n /**\n * Builds the primary match index from NSI category data.\n * After calling this method the matcher is ready to use via {@link match}.\n *\n * `data` must be an object keyed by `tree/key/value` paths, e.g.:\n * ```json\n * {\n * \"brands/amenity/bank\": { \"properties\": {}, \"items\": [ … ] },\n * \"brands/amenity/bar\": { \"properties\": {}, \"items\": [ … ] }\n * }\n * ```\n * (typically the cache built by `fileTree.read` or loaded from `dist/nsi.json`)\n *\n * @param data - NSI category data indexed by `tree/key/value` path\n */\n buildMatchIndex(data: NsiData): void {\n if (this.matchIndex) return; // it was built already\n\n const matchIndex = new Map<string, MatchIndexBranch>();\n this.matchIndex = matchIndex;\n\n const seenTree = new Map(); // warn if the same [k, v, nsimple] appears in multiple trees - #5625\n\n // For certain categories we do not want to match generic KV pairs like `building/yes` or `amenity/yes`\n const skipGenericKVMatches = (t: string, k: string, v: string): boolean => {\n return (\n t === 'flags' ||\n t === 'transit' ||\n k === 'landuse' ||\n v === 'atm' ||\n v === 'bicycle_parking' ||\n v === 'car_sharing' ||\n v === 'caravan_site' ||\n v === 'charging_station' ||\n v === 'dog_park' ||\n v === 'parking' ||\n v === 'phone' ||\n v === 'playground' ||\n v === 'post_box' ||\n v === 'public_bookcase' ||\n v === 'recycling' ||\n v === 'vending_machine'\n );\n };\n\n // Insert this item into the matchIndex\n const insertName = (which: 'primary' | 'alternate', t: string, kv: string, nsimple: string, itemID: string) => {\n if (!nsimple) {\n this.warnings.push(`Warning: skipping empty ${which} name for item ${t}/${kv}: ${itemID}`);\n return;\n }\n\n let branch = matchIndex.get(kv);\n if (!branch) {\n branch = {\n primary: new Map(),\n alternate: new Map(),\n excludeGeneric: new Map(),\n excludeNamed: new Map()\n };\n matchIndex.set(kv, branch);\n }\n\n let leaf = branch[which].get(nsimple);\n if (!leaf) {\n leaf = new Set();\n branch[which].set(nsimple, leaf);\n }\n\n leaf.add(itemID); // insert\n\n // check for duplicates - #5625\n if (!/yes$/.test(kv)) { // ignore genericKV like amenity/yes, building/yes, etc\n const kvnsimple = `${kv}/${nsimple}`;\n const existing = seenTree.get(kvnsimple);\n if (existing && existing !== t) {\n const items = Array.from(leaf);\n this.warnings.push(`Duplicate cache key \"${kvnsimple}\" in trees \"${t}\" and \"${existing}\", check items: ${items}`);\n return;\n }\n seenTree.set(kvnsimple, t);\n }\n };\n\n for (const tkv of Object.keys(data) as NsiPath[]) {\n const category = data[tkv];\n const parts = tkv.split('/', 3); // tkv = \"tree/key/value\"\n const t = parts[0] as NsiTree;\n const k = parts[1];\n const v = parts[2];\n const thiskv = `${k}/${v}`;\n const tree = trees[t];\n\n let branch = matchIndex.get(thiskv);\n if (!branch) {\n branch = {\n primary: new Map(),\n alternate: new Map(),\n excludeGeneric: new Map(),\n excludeNamed: new Map()\n };\n matchIndex.set(thiskv, branch);\n }\n\n // ADD EXCLUSIONS\n const properties = category.properties || {};\n const exclude = properties.exclude || {};\n for (const s of (exclude.generic || [])) branch.excludeGeneric.set(s, new RegExp(s, 'i'));\n for (const s of (exclude.named || [])) branch.excludeNamed.set(s, new RegExp(s, 'i'));\n const excludeRegexes = [...branch.excludeGeneric.values(), ...branch.excludeNamed.values()];\n\n\n // ADD ITEMS\n const items = category.items;\n if (!Array.isArray(items) || !items.length) continue;\n\n\n // Primary name patterns, match tags to take first\n // e.g. `name`, `name:ru`\n const primaryName = new RegExp(tree.nameTags.primary, 'i');\n\n // Alternate name patterns, match tags to consider after primary\n // e.g. `alt_name`, `short_name`, `brand`, `brand:ru`, etc..\n const alternateName = new RegExp(tree.nameTags.alternate, 'i');\n\n // There are a few exceptions to the name matching regexes.\n // Usually a tag suffix contains a language code like `name:en`, `name:ru`\n // but we want to exclude things like `operator:type`, `name:etymology`, etc..\n const notName = /:(colou?r|type|forward|backward|left|right|etymology|pronunciation|signed|wikipedia)$/i;\n\n // For certain categories we do not want to match generic KV pairs like `building/yes` or `amenity/yes`\n const skipGenericKV = skipGenericKVMatches(t, k, v);\n\n // We will collect the generic KV pairs anyway (for the purpose of filtering them out of matchTags)\n const genericKV = new Set([`${k}/yes`, `building/yes`]);\n\n // Collect alternate tagpairs for this kv category from matchGroups.\n // We might also pick up a few more generic KVs (like `shop/yes`)\n const matchGroupKV = new Set();\n for (const matchGroup of Object.values(matchGroups)) {\n const inGroup = matchGroup.some(otherkv => otherkv === thiskv);\n if (!inGroup) continue;\n\n for (const otherkv of matchGroup) {\n if (otherkv === thiskv) continue; // skip self\n matchGroupKV.add(otherkv);\n\n const otherk = otherkv.split('/', 2)[0]; // we might pick up a `shop/yes`\n genericKV.add(`${otherk}/yes`);\n }\n }\n\n // For each item, insert all [key, value, name] combinations into the match index\n for (const item of items) {\n if (!item.id) continue;\n\n // Automatically remove redundant `matchTags` - #3417, #8137\n // (i.e. This kv is already covered by matchGroups, so it doesn't need to be in `item.matchTags`\n // or this kv is the primary kv, so it doesn't need to be duplicated in `item.matchTags`)\n if (Array.isArray(item.matchTags) && item.matchTags.length) {\n item.matchTags = item.matchTags\n .filter(matchTag => !matchGroupKV.has(matchTag) && (matchTag !== thiskv) && !genericKV.has(matchTag));\n\n if (!item.matchTags.length) delete item.matchTags;\n }\n\n // key/value tagpairs to insert into the match index..\n let kvTags = [`${thiskv}`]\n .concat(item.matchTags || []);\n\n if (!skipGenericKV) {\n kvTags = kvTags\n .concat(Array.from(genericKV)); // #3454 - match some generic tags\n }\n\n // Index all the namelike tag values\n for (const osmkey of Object.keys(item.tags)) {\n if (notName.test(osmkey)) continue; // osmkey is not a namelike tag, skip\n const osmvalue = item.tags[osmkey];\n if (!osmvalue || excludeRegexes.some(regex => regex.test(osmvalue))) continue; // osmvalue missing or excluded\n\n if (primaryName.test(osmkey)) {\n for (const kv of kvTags) insertName('primary', t, kv, simplify(osmvalue), item.id);\n } else if (alternateName.test(osmkey)) {\n for (const kv of kvTags) insertName('alternate', t, kv, simplify(osmvalue), item.id);\n }\n }\n\n // Index `matchNames` after indexing all other names..\n const keepMatchNames = new Set<string>();\n for (const matchName of (item.matchNames || [])) {\n // If this matchname isn't already indexed, add it to the alternate index\n const nsimple = simplify(matchName);\n for (const kv of kvTags) {\n const branch = matchIndex.get(kv);\n const primaryLeaf = branch && branch.primary.get(nsimple);\n const alternateLeaf = branch && branch.alternate.get(nsimple);\n const inPrimary = primaryLeaf && primaryLeaf.has(item.id);\n const inAlternate = alternateLeaf && alternateLeaf.has(item.id);\n\n if (!inPrimary && !inAlternate) {\n insertName('alternate', t, kv, nsimple, item.id);\n keepMatchNames.add(matchName);\n }\n }\n }\n\n // Automatically remove redundant `matchNames` - #3417\n // (i.e. This name got indexed some other way, so it doesn't need to be in `item.matchNames`)\n if (keepMatchNames.size) {\n item.matchNames = Array.from(keepMatchNames);\n } else {\n delete item.matchNames;\n }\n\n } // each item\n } // each tkv\n }\n\n\n /**\n * Registers every item's `locationSet` with the supplied {@link LocationConflation}\n * instance so that {@link match} can do location-aware filtering. This is optional —\n * skip it if you don't need location-aware matching.\n *\n * Under the hood this just calls `loco.registerLocationSets(items)`, which:\n * - assigns `item.locationSetID` in place (e.g. `'+[Q30]'`),\n * - builds an inverted spatial index without resolving combined polygons,\n * - is tolerant of bad/empty locationSets (falls back to world).\n *\n * `data` must be an object keyed by `tree/key/value` paths (same format as\n * {@link buildMatchIndex}).\n *\n * @param data - NSI category data indexed by `tree/key/value` path\n * @param loco - Optional `LocationConflation` instance used to index locationSets.\n * If omitted, a new bare instance is created internally. Callers that have their\n * own configured instance (e.g. with a FeatureCollection of custom `.geojson`\n * features) should pass it in so indexing and lookups share the same cache.\n * Whichever instance is used, the matcher keeps a reference and delegates\n * `locationSetsAt` / `getLocationSetArea` calls to it at match time.\n */\n buildLocationIndex(data: NsiData, loco?: LocationResolver): void {\n loco = loco ?? new LocationConflation();\n this.loco = loco;\n\n const itemLocationSetID = new Map<string, LocationSetID>();\n this.itemLocationSetID = itemLocationSetID;\n\n for (const tkv of Object.keys(data) as NsiPath[]) {\n const items = data[tkv].items;\n if (!Array.isArray(items) || !items.length) continue;\n const registered = loco.registerLocationSets(items);\n for (const item of registered) {\n itemLocationSetID.set(item.id, item.locationSetID);\n }\n }\n }\n\n\n /**\n * Matches a `[key, value, name]` tuple against the index and returns results.\n *\n * **Case 1 — canonical match:**\n * Returns an array of {@link Hit} objects sorted by match quality:\n * - `\"primary\"` hits (matches `name` tag) come first,\n * - `\"alternate\"` hits (matches `alt_name`, `brand`, etc.) come second.\n *\n * Within each group, results are sorted by area:\n * - **area descending** (worldwide → local) when no `loc` is given,\n * - **area ascending** (local → worldwide) when `loc` is given.\n *\n * Each hit includes the item's `area` in km².\n *\n * **Case 2 — exclude match:**\n * Returns a single-element array with either:\n * - `{ match: 'excludeGeneric', pattern, kv }` — a generic word (e.g. \"Food Court\")\n * that is probably not a real name.\n * - `{ match: 'excludeNamed', pattern, kv }` — a real but common name (e.g. \"Kebabai\")\n * that is not a brand.\n *\n * **Case 3 — no match:**\n * Returns `null`.\n *\n * @param k - OSM key (e.g. `\"amenity\"`)\n * @param v - OSM value (e.g. `\"bank\"`)\n * @param n - A name-like string to look up (e.g. `\"Wells Fargo\"`)\n * @param loc - Optional `[lon, lat]` coordinate to restrict results by location\n * @returns An array of {@link Hit} results, or `null` if nothing matched.\n * @throws {Error} If the match index has not been built yet.\n */\n match(k: string, v: string, n: string, loc?: Vec2): Array<MatchHit> | null {\n if (!this.matchIndex) {\n throw new Error('match: matchIndex not built.');\n }\n const matchIndex = this.matchIndex;\n const loco = this.loco;\n const itemLocationSetID = this.itemLocationSetID;\n\n // If we were supplied a location, and the location index has been set up,\n // get the locationSetIDs that are valid there so we can filter results.\n let validHere: Map<LocationSetID, number> | null = null;\n if (Array.isArray(loc) && loco) {\n validHere = loco.locationSetsAt(loc);\n }\n\n const nsimple = simplify(n);\n\n const seen = new Set();\n const results: Array<MatchHit> = [];\n\n // Sort smaller (more local) locations first.\n const byAreaAscending = (hitA: MatchHit, hitB: MatchHit): number => {\n return (hitA.area || 0) - (hitB.area || 0);\n };\n // Sort larger (more worldwide) locations first.\n const byAreaDescending = (hitA: MatchHit, hitB: MatchHit): number => {\n return (hitB.area || 0) - (hitA.area || 0);\n };\n\n const isValidLocation = (hit: MatchHit) => {\n if (!validHere || !itemLocationSetID) return true;\n const locationSetID = itemLocationSetID.get(hit.itemID!);\n return locationSetID ? validHere.has(locationSetID) : false;\n };\n\n const tryMatch = (which: 'primary' | 'alternate' | 'exclude', kv: string): boolean => {\n const branch = matchIndex.get(kv);\n if (!branch) return false;\n\n if (which === 'exclude') { // Test name `n` against named and generic exclude patterns\n let regex = [...branch.excludeNamed.values()].find(regex => regex.test(n));\n if (regex) {\n results.push({ match: 'excludeNamed', pattern: String(regex), kv: kv });\n return false;\n }\n regex = [...branch.excludeGeneric.values()].find(regex => regex.test(n));\n if (regex) {\n results.push({ match: 'excludeGeneric', pattern: String(regex), kv: kv });\n return false;\n }\n return false;\n }\n\n const leaf = branch[which].get(nsimple);\n if (!leaf || !leaf.size) return false;\n if (!(which === 'primary' || which === 'alternate')) return false;\n\n // If we get here, we matched something..\n // Prepare the results, calculate areas (if location index was set up)\n let hits: Array<MatchHit> = [];\n for (const itemID of [...leaf]) {\n let area = Infinity;\n if (loco && itemLocationSetID) {\n const setID = itemLocationSetID.get(itemID);\n if (setID) {\n area = loco.getLocationSetArea(setID) ?? Infinity;\n }\n }\n hits.push({ match: which, itemID: itemID, area: area, kv: kv, nsimple: nsimple });\n }\n\n let sortFn = byAreaDescending;\n\n // Filter the match to include only results valid in the requested `loc`..\n if (validHere) {\n hits = hits.filter(isValidLocation);\n sortFn = byAreaAscending;\n }\n\n if (!hits.length) return false;\n\n // push results\n for (const hit of hits.sort(sortFn)) {\n if (seen.has(hit.itemID)) continue;\n seen.add(hit.itemID);\n results.push(hit);\n }\n\n return true;\n };\n\n const gatherResults = (which: 'primary' | 'alternate' | 'exclude'): void => {\n // First try an exact match on k/v\n const kv = `${k}/${v}`;\n let didMatch = tryMatch(which, kv);\n if (didMatch) return;\n\n // If that didn't work, look in match groups for other pairs considered equivalent to k/v..\n for (const matchGroup of Object.values(matchGroups)) {\n const inGroup = matchGroup.some(otherkv => otherkv === kv);\n if (!inGroup) continue;\n\n for (const otherkv of matchGroup) {\n if (otherkv === kv) continue; // skip self\n didMatch = tryMatch(which, otherkv);\n if (didMatch) return;\n }\n }\n\n // If finished 'exclude' pass and still haven't matched anything, try the global `genericWords.json` patterns\n if (which === 'exclude') {\n const regex = [...this.genericWords.values()].find(regex => regex.test(n));\n if (regex) {\n results.push({ match: 'excludeGeneric', pattern: String(regex) }); // note no `branch`, no `kv`\n return;\n }\n }\n };\n\n gatherResults('primary');\n gatherResults('alternate');\n if (results.length) return results;\n\n gatherResults('exclude');\n return results.length ? results : null;\n }\n\n\n /**\n * Returns any warnings discovered while building the match index\n * (e.g. duplicate cache keys across trees).\n *\n * @returns An array of warning message strings (may be empty).\n */\n getWarnings(): Array<string> {\n return this.warnings;\n }\n}\n",
"import diacritics from 'diacritics';\n\n/**\n * Simplifies a string by removing spaces, punctuation, and diacritics,\n * replacing `&` with `and`, and lowercasing the result.\n * Useful for generating normalized keys that can be compared across languages and scripts.\n *\n * For punctuation ranges see https://stackoverflow.com/a/21224179\n *\n * @param str - The input string to simplify\n * @returns A normalized, lowercase string with all whitespace, punctuation, and diacritics removed.\n * Returns an empty string if the input is not a string.\n */\nexport function simplify(str?: string): string {\n if (typeof str !== 'string') return '';\n\n return diacritics.remove(\n str\n .replace(/&/g, 'and')\n .replace(/(İ|i̇)/ig, 'i') // for BİM, İşbank - NSI#5017, NSI#8261\n .replace(/[\\s\\-=_!\"#%'*{},.\\/:;?\\(\\)\\[\\]@\\\\$\\^*+<>«»~`’\\u00a1\\u00a7\\u00b6\\u00b7\\u00bf\\u037e\\u0387\\u055a-\\u055f\\u0589\\u05c0\\u05c3\\u05c6\\u05f3\\u05f4\\u0609\\u060a\\u060c\\u060d\\u061b\\u061e\\u061f\\u066a-\\u066d\\u06d4\\u0700-\\u070d\\u07f7-\\u07f9\\u0830-\\u083e\\u085e\\u0964\\u0965\\u0970\\u0af0\\u0df4\\u0e4f\\u0e5a\\u0e5b\\u0f04-\\u0f12\\u0f14\\u0f85\\u0fd0-\\u0fd4\\u0fd9\\u0fda\\u104a-\\u104f\\u10fb\\u1360-\\u1368\\u166d\\u166e\\u16eb-\\u16ed\\u1735\\u1736\\u17d4-\\u17d6\\u17d8-\\u17da\\u1800-\\u1805\\u1807-\\u180a\\u1944\\u1945\\u1a1e\\u1a1f\\u1aa0-\\u1aa6\\u1aa8-\\u1aad\\u1b5a-\\u1b60\\u1bfc-\\u1bff\\u1c3b-\\u1c3f\\u1c7e\\u1c7f\\u1cc0-\\u1cc7\\u1cd3\\u2000-\\u206f\\u2cf9-\\u2cfc\\u2cfe\\u2cff\\u2d70\\u2e00-\\u2e7f\\u3001-\\u3003\\u303d\\u30fb\\ua4fe\\ua4ff\\ua60d-\\ua60f\\ua673\\ua67e\\ua6f2-\\ua6f7\\ua874-\\ua877\\ua8ce\\ua8cf\\ua8f8-\\ua8fa\\ua92e\\ua92f\\ua95f\\ua9c1-\\ua9cd\\ua9de\\ua9df\\uaa5c-\\uaa5f\\uaade\\uaadf\\uaaf0\\uaaf1\\uabeb\\ufe10-\\ufe16\\ufe19\\ufe30\\ufe45\\ufe46\\ufe49-\\ufe4c\\ufe50-\\ufe52\\ufe54-\\ufe57\\ufe5f-\\ufe61\\ufe68\\ufe6a\\ufe6b\\ufeff\\uff01-\\uff03\\uff05-\\uff07\\uff0a\\uff0c\\uff0e\\uff0f\\uff1a\\uff1b\\uff1f\\uff20\\uff3c\\uff61\\uff64\\uff65]+/g,'')\n .toLowerCase()\n );\n}\n",
"import { simplify } from './simplify.ts';\n\n\n/**\n * Removes common \"noise\" words from a name string and then simplifies the result.\n * Used to generate a stem for catching near-duplicate names\n * (e.g. \"First National Bank\" and \"First National\" would produce the same stem).\n *\n * Noise words removed: bank/banc/banco, банк, coop, express, gas/fuel, wireless, shop/store.\n *\n * @param str - The input name string to stem\n * @returns A simplified, de-noised string. Returns an empty string if the input is not a string.\n */\nexport function stemmer(str?: string): string {\n if (typeof str !== 'string') return '';\n\n const noise = [\n /ban(k|c)(a|o)?/ig,\n /банк/ig,\n /coop/ig,\n /express/ig,\n /(gas|fuel)/ig,\n /wireless/ig,\n /(shop|store)/ig\n ];\n\n str = noise.reduce((acc, regex) => acc.replace(regex, ''), str);\n return simplify(str);\n}\n",
"const withLocale = new Intl.Collator('en-US').compare; // specify 'en-US' for stable sorting\n\n\n/**\n * Returns a shallow copy of an object with its keys sorted and any array values sorted.\n * Keys that look like Wikidata QIDs (e.g. `Q42`) are sorted numerically;\n * all other keys are sorted with en-US locale collation.\n * This is useful for producing deterministic JSON output for file diffing.\n *\n * @param obj - The input object to sort\n * @returns A new object with sorted keys and sorted array values, or `null` if the input is falsy.\n */\nexport function sortObject<T extends object>(obj: T): T;\nexport function sortObject<T extends Record<string, unknown>>(obj: T): T | null {\n if (!obj) return null;\n\n const sorted: Record<string, unknown> = {};\n const keys = Object.keys(obj).sort(keyCompare);\n for (const k of keys) {\n const v = obj[k];\n sorted[k] = Array.isArray(v) ? v.sort(withLocale) : v;\n }\n return sorted as T;\n\n\n /**\n * Compares two object keys, sorting Wikidata QIDs (`Q123`) numerically\n * and everything else with en-US locale collation.\n *\n * @param a - First key\n * @param b - Second key\n * @returns Negative if a < b, positive if a > b, zero if equal\n */\n function keyCompare(a: string, b: string): number {\n const qid = /^Q(\\d+)$/;\n const aMatch = a.match(qid);\n const bMatch = b.match(qid);\n if (aMatch && bMatch) {\n return parseInt(aMatch[1], 10) - parseInt(bMatch[1], 10); // sort QIDs numerically\n } else {\n return withLocale(a, b);\n }\n }\n}\n",
"import type { Preset, Presets } from '@openstreetmap/id-tagging-schema';\nimport { sortObject } from './sort_object.ts';\n\nimport type {\n DissolvedMap,\n NsiData,\n NsiPath,\n NsiTree,\n NsiTreeProperties,\n OsmTags,\n IDPreset,\n WikidataMap\n} from './types.ts';\n\nconst withLocale = new Intl.Collator('en-US').compare; // specify 'en-US' for stable sorting\n\n// Imported JSON (will be inlined by bun)\nimport treesJSON from '../config/trees.json' with {type: 'json'};\nconst trees: Record<NsiTree, NsiTreeProperties> = treesJSON.trees;\n\n\n/** Options for {@link buildIDPresets}. */\nexport interface BuildIDPresetsOptions {\n /** The id-tagging-schema source presets dictionary (the `presets` field of `presets.json`). */\n sourcePresets: Presets;\n /** Map of QID → wikidata info, used to source preset `imageURL` values. */\n wikidata?: WikidataMap;\n /** Map of NSI item id → dissolved record. Items present here become non-searchable. */\n dissolved?: DissolvedMap;\n}\n\n/** Result of {@link buildIDPresets}. */\nexport interface BuildIDPresetsResult {\n /** The generated iD/Rapid presets, keyed by `<presetID>/<itemID>`. */\n presets: Record<string, IDPreset>;\n /** Sorted list of NSI `tree/key/value` paths for which no source iD preset was found. */\n missing: NsiPath[];\n}\n\n\n// Exceptions where the NSI `key/value` doesn't match the iD preset path `key/value`.\n// See also https://github.com/openstreetmap/iD/issues/11527\n// id-tagging-schema occasionally moves their presets around, changing their presetIDs.\nconst presetPathOverrides: Record<string, string> = {\n 'highway/bus_stop': 'public_transport/platform/bus_point',\n 'amenity/ferry_terminal': 'public_transport/station_ferry',\n 'amenity/college': 'education/college',\n 'amenity/driving_school': 'education/driving_school',\n 'amenity/dancing_school': 'education/dancing_school',\n 'amenity/kindergarten': 'education/kindergarten',\n 'amenity/language_school': 'education/language_school',\n 'amenity/music_school': 'education/music_school',\n 'amenity/prep_school': 'education/prep_school',\n 'amenity/school': 'education/school',\n 'amenity/university': 'education/university',\n 'emergency/water_rescue': 'emergency/lifeboat_station'\n};\n\n// Tags that NSI allows to process as multi-valued\nconst semicolonSplittedKeys = ['beauty', 'clothes', 'cuisine', 'healthcare:speciality', 'social_facility', 'sport', 'vending', 'waste'];\n\n// Prefer a wiki commons logo for these QIDs.\n// Related issues list: iD#6361, NSI#2798, NSI#3122, NSI#8042, NSI#8373\nconst preferCommons: Record<string, boolean> = {\n Q177054: true, // Burger King\n Q524757: true, // KFC\n Q779845: true, // CBA\n Q1205312: true, // In-N-Out\n Q10443115: true, // Carlings\n Q38076: true // McDonald's\n};\n\n\n/** Resolves the iD preset path to look under for a given NSI tkv. */\nfunction resolvePresetPath(tkv: NsiPath, k: string, v: string, kv: string, ferryIndex: number): string {\n if (tkv === 'transit/route/ferry') {\n return ferryIndex === 0 ? 'type/route/ferry' : 'route/ferry';\n }\n if (k === 'route') return `type/route/${v}`;\n return presetPathOverrides[kv] || kv;\n}\n\n\n/** Picks the most specific iD preset matching an NSI item's tags. */\nfunction pickBestChildPreset(\n childPresets: Map<string, Preset>,\n tags: OsmTags\n): { presetID?: string; preset?: Preset } {\n if (childPresets.size === 0) return {};\n\n if (childPresets.size === 1) {\n const entry = childPresets.entries().next().value;\n return entry ? { presetID: entry[0], preset: entry[1] } : {};\n }\n\n // The best iD preset for an NSI entry is determined by count of tags that have\n // matched (more is better) and position for multi-value tags (e.g. cuisine)\n let matchTagsCount = 0;\n let matchSemicolonRating = 0;\n let matchPresetPath: string | undefined;\n let matchPreset: Preset | undefined;\n\n for (const [checkPresetPath, checkPreset] of childPresets) {\n const checkPresetTags = Object.entries(checkPreset.tags as OsmTags);\n let currentMatchSemicolonRating = 0;\n\n const isPresetMatch = checkPresetTags.every(kv => {\n const osmKey = kv[0];\n const osmVal = kv[1];\n\n const nsiVal = tags[osmKey];\n if (!nsiVal) return false;\n\n if (semicolonSplittedKeys.includes(osmKey)) {\n const vals = nsiVal.split(';');\n const findResult = vals.indexOf(osmVal);\n if (findResult === -1) return false;\n // For a smaller element index rating will be higher\n currentMatchSemicolonRating -= findResult;\n return true;\n }\n return (osmVal === nsiVal);\n });\n\n // If rating of current element is higher than the saved one, we overwrite saved\n if (isPresetMatch && (\n (checkPresetTags.length > matchTagsCount) ||\n (checkPresetTags.length === matchTagsCount && currentMatchSemicolonRating > matchSemicolonRating)\n )) {\n matchTagsCount = checkPresetTags.length;\n matchSemicolonRating = currentMatchSemicolonRating;\n matchPresetPath = checkPresetPath;\n matchPreset = checkPreset;\n }\n }\n\n return matchPreset && matchPresetPath\n ? { presetID: matchPresetPath, preset: matchPreset }\n : {};\n}\n\n\n/** Picks a logo URL from wikidata for a given QID. */\nfunction pickLogoURL(qid: string, wikidata: WikidataMap): string | undefined {\n const logoURLs = wikidata[qid] && wikidata[qid].logos;\n if (!logoURLs) return undefined;\n if (logoURLs.wikidata && preferCommons[qid]) return logoURLs.wikidata;\n if (logoURLs.facebook) return logoURLs.facebook;\n return logoURLs.wikidata;\n}\n\n\n/**\n * Collects search terms for an NSI item — its matchNames plus name-like tag values.\n */\nfunction collectTerms(\n item: { matchNames?: string[]; tags: OsmTags },\n primaryName: RegExp,\n alternateName: RegExp,\n notName: RegExp\n): Set<string> {\n const terms = new Set(item.matchNames || []);\n for (const osmkey of Object.keys(item.tags)) {\n if (osmkey === 'name') continue; // exclude `name` tag, as iD prioritizes it above `preset.terms` already\n if (notName.test(osmkey)) continue; // osmkey is not a namelike tag, skip\n if (primaryName.test(osmkey) || alternateName.test(osmkey)) {\n terms.add(item.tags[osmkey].toLowerCase());\n }\n }\n return terms;\n}\n\n\n/**\n * Returns the `fields` array for an NSI preset when `^name` is being preserved,\n * or `undefined` otherwise.\n *\n * If we're preserving the `name` tag, make sure both \"name\" and \"brand\"/\"operator\"\n * fields are shown. This triggers iD to lock the brand/operator field but allow\n * edits to the \"name\" field.\n */\nfunction buildFields(t: string, preset: Preset & { originalFields?: string[] }, preserveTags: string[]): string[] {\n const fields = preset.originalFields || preset.fields || [];\n\n if (!preserveTags.some(s => s === '^name')) return fields;\n // `originalFields` is preferred over `fields` to be backwards compatible with old versions\n // of iD (released between May 2026 and July 2026). This can eventually be removed once there\n // is no one using these old versions of iD.\n if (t === 'brands') return ['name', 'brand', ...fields];\n if (t === 'operators') return ['name', 'operator', ...fields];\n return fields;\n}\n\n\n/**\n * Build iD/Rapid presets from NSI data.\n *\n * This is a pure function: it does no I/O, performs no console output, and does not\n * mutate any of its inputs. Suitable for use in browser-based downstream projects\n * that fetch the NSI data on-the-fly.\n *\n * @param data - NSI category data indexed by `tree/key/value` path (the `_nsi.path` cache, or `nsi.json`'s `nsi` field)\n * @param opts - Sources for id-tagging-schema presets, wikidata logos, and dissolutions\n * @returns the generated presets plus a list of paths missing a source iD preset\n */\nexport function buildIDPresets(data: NsiData, opts: BuildIDPresetsOptions): BuildIDPresetsResult {\n const sourcePresets = opts.sourcePresets;\n const wikidata = opts.wikidata || {};\n const dissolved = opts.dissolved || {};\n\n //\n // First we'll match every NSI item to a source iD preset.\n // The source iD presets look like this:\n //\n // \"amenity\": {\n // \"name\": \"Amenity\"\n // \"fields\": […],\n // \"geometry\": […],\n // \"tags\": {\n // \"amenity\": \"*\"\n // },\n // \"searchable\": false\n // },\n // \"amenity/fast_food\": {\n // \"name\": \"Fast Food\",\n // \"icon\": \"maki-fast-food\",\n // \"fields\": […],\n // \"geometry\": […],\n // \"terms\": […],\n // \"tags\": {\n // \"amenity\": \"fast_food\"\n // }\n // },\n // \"amenity/fast_food/sandwich\": {\n // \"name\": \"Sandwich Fast Food\",\n // \"icon\": \"temaki-sandwich\",\n // \"fields\": […],\n // \"geometry\": […],\n // \"terms\": […],\n // \"tags\": {\n // \"amenity\": \"fast_food\",\n // \"cuisine\": \"sandwich\"\n // }\n // },\n //\n // There are a few special behaviors in the iD presets are important to us:\n // - They each have stable identifiers like `key`, `key/value`, `key/value/anothervalue`\n // - Presets with increasing specificity \"inherit\" fields from presets of less specificity\n // (e.g. the sandwich fast food preset inherits all the fields of the regular fast food preset)\n // - We can generate presets with NSI identifiers that hang off the end of this specificity chain\n // (e.g. \"amenity/fast_food/sandwich/arbys-3c08fb\")\n // - NSI identifiers will not collide with the preset identifiers (NSI ids don't look like tag values)\n //\n\n const targetPresets: Record<string, Preset> = {};\n const missing = new Set<string>();\n const paths = Object.keys(data);\n\n // Ferry hack! ⛴\n // Append a duplicate tkv path for Ferry routes so we can generate them twice..\n // These actually exist as 2 iD presets:\n // `type/route/ferry` - for a Route Relation\n // `route/ferry` - for a Way\n let ferryCount = 0;\n if (data['transit/route/ferry']) {\n paths.push('transit/route/ferry'); // add a duplicate tkv\n }\n\n for (const tkv of paths.sort(withLocale) as NsiPath[]) {\n const properties = data[tkv].properties || {};\n const items = data[tkv].items;\n if (!Array.isArray(items) || !items.length) continue;\n\n const [t, k, v] = tkv.split('/', 3); // tkv = \"tree/key/value\"\n const tree = trees[t as NsiTree];\n const kv = `${k}/${v}`;\n\n // Ferry hack! ⛴ - duplicated tkv generates `type/route/ferry` then `route/ferry`\n const ferryIndex = (tkv === 'transit/route/ferry') ? ferryCount++ : 0;\n const presetPath = resolvePresetPath(tkv, k, v, kv, ferryIndex);\n\n // Which wikidata tag is considered the \"main\" tag for this tree?\n const wdTag = tree.mainTag;\n\n // Primary/alternate names may be used as preset search terms\n const primaryName = new RegExp(tree.nameTags.primary, 'i');\n const alternateName = new RegExp(tree.nameTags.alternate, 'i');\n\n // There are a few exceptions to the name matching regexes.\n // Usually a tag suffix contains a language code like `name:en`, `name:ru`\n // but we want to exclude things like `operator:type`, `name:etymology`, etc..\n // NOTE: here we intentionally exclude `:wikidata`, in `matcher.ts` we do not.\n const notName = /:(colour|type|left|right|etymology|pronunciation|wikipedia|wikidata)$/i;\n\n // Look for iD presets that would fit this NSI presetPath.\n const childPresets = new Map<string, Preset>();\n for (const checkPath in sourcePresets) {\n if (checkPath.startsWith(presetPath)) {\n childPresets.set(checkPath, sourcePresets[checkPath]);\n }\n }\n\n for (const item of items) {\n const tags = item.tags;\n const qid = tags[wdTag];\n if (!qid || !/^Q\\d+$/.test(qid)) continue; // wikidata tag missing or looks wrong..\n\n // Sometimes we can choose a more specific iD preset than `key/value`,\n // otherwise fall back to a generic like `amenity/yes`, `shop/yes`.\n let { presetID, preset } = pickBestChildPreset(childPresets, tags);\n if (!preset) {\n presetID = k;\n preset = sourcePresets[presetID];\n missing.add(tkv);\n }\n if (!preset) continue; // *still* no match - bail out\n\n // Gather search terms - include all primary/alternate names and matchNames\n // (There is similar code in lib/matcher.ts)\n const terms = collectTerms(item, primaryName, alternateName, notName);\n\n // generate our target preset\n const targetID = `${presetID}/${item.id}`;\n\n const targetTags: OsmTags = {};\n targetTags[wdTag] = tags[wdTag]; // add the `*:wikidata` tag\n for (const presetKey in preset.tags) { // prioritize NSI tags over iD preset tags (for `vending`, `cuisine`, etc)\n targetTags[presetKey] = tags[presetKey] || preset.tags[presetKey];\n }\n\n const logoURL = pickLogoURL(qid, wikidata);\n\n const preserveTags = item.preserveTags || properties.preserveTags || [];\n\n const targetPreset: IDPreset = {\n name: item.displayName,\n locationSet: item.locationSet as {} & IDPreset['locationSet'],\n icon: preset.icon!,\n geometry: preset.geometry,\n fields: buildFields(t, preset, preserveTags),\n moreFields: preset.moreFields || [],\n tags: sortObject(targetTags),\n matchScore: 2\n };\n\n if (logoURL) targetPreset.imageURL = logoURL;\n if (terms.size) targetPreset.terms = Array.from(terms).sort(withLocale);\n if (preset.reference) targetPreset.reference = preset.reference;\n if (dissolved[item.id]) targetPreset.searchable = false; // dissolved/closed businesses\n if (preserveTags.length) targetPreset.preserveTags = preserveTags; // see NSI#10083\n\n targetPreset.tags = sortObject(targetTags) as OsmTags;\n targetPreset.addTags = sortObject(Object.assign({}, item.tags, targetTags)) as OsmTags;\n\n targetPresets[targetID] = targetPreset;\n }\n }\n\n return {\n presets: targetPresets,\n missing: Array.from(missing).sort(withLocale)\n };\n}\n",
"import XMLBuilder from 'fast-xml-builder';\n\nimport type { DissolvedMap, NsiData, NsiPath, NsiTree, NsiTreeProperties } from './types.ts';\nimport type { XmlBuilderOptions } from 'fast-xml-builder';\n\nconst xmlBuilderOptions = {\n ignoreAttributes: false,\n suppressEmptyNode: true\n} satisfies XmlBuilderOptions;\n\n// Imported JSON (will be inlined by bun)\nimport treesJSON from '../config/trees.json' with {type: 'json'};\n\nconst trees: Record<NsiTree, NsiTreeProperties> = treesJSON.trees;\nconst withLocale = new Intl.Collator('en-US').compare; // specify 'en-US' for stable sorting\n\n\n/** Options for {@link buildJOSMPresets}. */\nexport interface BuildJOSMPresetsOptions {\n /** Project version stamped into the `<presets version=\"...\">` attribute. */\n version: string;\n /** Project description stamped into the `<presets description=\"...\">` attribute. */\n description: string;\n /** Map of NSI item id → dissolved record. Items present here are excluded from the output. */\n dissolved?: DissolvedMap;\n}\n\n/** Options for {@link JOSMPresetsSerializer.serialize}. */\nexport interface JOSMPresetsSerializerOptions {\n /** If `true`, produce indented, human-readable XML; if absent or `false`, produce compact single-line XML. */\n prettyPrint?: boolean;\n}\n\n/**\n * Returned by {@link buildJOSMPresets}. Holds the built-up preset data and can\n * serialize it to XML on demand, with or without pretty-printing.\n */\nexport interface JOSMPresetsSerializer {\n /**\n * Serialize the presets to an XML string beginning with the UTF-8 declaration.\n * @param opts - Serialization options\n * @returns An XML string. Pass `{ prettyPrint: true }` for indented output;\n * omit or pass `false` for compact single-line output.\n */\n serialize(opts?: JOSMPresetsSerializerOptions): string;\n}\n\ninterface JOSMKeyXML {\n '@_key': string;\n '@_value': string;\n}\n\ninterface JOSMItemXML {\n '@_name': string;\n '@_type': string;\n key: JOSMKeyXML[];\n}\n\ninterface JOSMBranchGroupXML {\n '@_name': string;\n group: JOSMGroupXML[];\n}\n\ninterface JOSMLeafGroupXML {\n '@_name': string;\n item: JOSMItemXML[];\n}\n\ntype JOSMGroupXML = JOSMBranchGroupXML | JOSMLeafGroupXML;\n\ninterface JOSMPresetsXML {\n '?xml': {\n '@_version': '1.0';\n '@_encoding': 'UTF-8';\n };\n presets: {\n '@_xmlns': string;\n '@_author': string;\n '@_shortdescription': string;\n '@_description': string;\n '@_link': string;\n '@_version': string;\n group: JOSMBranchGroupXML;\n };\n}\n\n\n/**\n * Build JOSM tagging presets from NSI data, organised into nested groups\n * by `tree → key → value`.\n *\n * Returns a {@link JOSMPresetsSerializer}; call `result.serialize({ prettyPrint: true })`\n * for indented output or `result.serialize()` for compact single-line output.\n *\n * This is a pure function: it does no I/O, performs no console output, and does not\n * mutate any of its inputs.\n *\n * @see https://josm.openstreetmap.de/wiki/TaggingPresets\n *\n * @param data - NSI category data indexed by `tree/key/value` path\n * @param opts - Project metadata and optional dissolution map\n * @returns A serializer that produces either pretty-printed or minified XML\n */\nexport function buildJOSMPresets(data: NsiData, opts: BuildJOSMPresetsOptions): JOSMPresetsSerializer {\n const dissolved = opts.dissolved || {};\n\n const topGroup: JOSMBranchGroupXML = { '@_name': 'Name Suggestion Index', group: [] };\n const xml: JOSMPresetsXML = {\n '?xml': { '@_version': '1.0', '@_encoding': 'UTF-8' },\n presets: {\n '@_xmlns': 'http://josm.openstreetmap.de/tagging-preset-1.0',\n '@_author': 'Name Suggestion Index',\n '@_shortdescription': 'Name Suggestion Index',\n '@_description': opts.description,\n '@_link': 'https://github.com/osmlab/name-suggestion-index',\n '@_version': opts.version,\n group: topGroup\n }\n };\n\n let tPrev, kPrev, vPrev;\n let tGroup: JOSMBranchGroupXML | undefined;\n let kGroup: JOSMBranchGroupXML | undefined;\n let vGroup: JOSMLeafGroupXML | undefined;\n\n const paths = Object.keys(data).sort(withLocale) as NsiPath[];\n for (const tkv of paths) {\n const [t, k, v] = tkv.split('/', 3); // tkv = \"tree/key/value\"\n\n // Which wikidata tag is considered the \"main\" tag for this tree?\n const wdTag = trees[t as NsiTree].mainTag;\n\n // Include only items that have a wikidata tag and are not dissolved..\n const items = (data[tkv].items || [])\n .filter(item => {\n const qid = item.tags[wdTag];\n if (!qid || !/^Q\\d+$/.test(qid)) return false; // wikidata tag missing or looks wrong..\n if (dissolved[item.id]) return false; // dissolved/closed businesses..\n return true;\n });\n\n if (!items.length) continue; // skip this path\n\n // Create new menu groups as t/k/v change\n const tChanged = t !== tPrev;\n const kChanged = tChanged || k !== kPrev;\n const vChanged = kChanged || v !== vPrev;\n\n if (tChanged) {\n tGroup = { '@_name': t, group: [] };\n topGroup.group.push(tGroup);\n }\n if (kChanged) {\n kGroup = { '@_name': k, group: [] };\n tGroup!.group.push(kGroup);\n }\n if (vChanged) {\n vGroup = { '@_name': v, item: [] };\n kGroup!.group.push(vGroup);\n }\n\n // Choose allowable geometries for the category\n let presetType;\n if (t === 'flags') {\n presetType = 'node';\n } else if (k === 'route') {\n if (v === 'ferry') { // Ferry hack! ⛴\n presetType = 'way,closedway,relation';\n } else {\n presetType = 'relation';\n }\n } else if (k === 'power' && (v === 'line' || v === 'minor_line')) {\n presetType = 'way,closedway';\n } else if (k === 'power' && (v === 'pole' || v === 'tower')) {\n presetType = 'node';\n } else {\n presetType = 'node,closedway,multipolygon'; // default for POIs\n }\n\n for (const item of items) {\n vGroup!.item.push({\n '@_name': item.displayName,\n '@_type': presetType,\n key: Object.entries(item.tags).map(([osmkey, osmvalue]) => ({\n '@_key': osmkey,\n '@_value': osmvalue\n }))\n });\n }\n\n tPrev = t;\n kPrev = k;\n vPrev = v;\n }\n\n return {\n serialize(opts?: JOSMPresetsSerializerOptions): string {\n const builder = new XMLBuilder({\n ...xmlBuilderOptions,\n format: opts?.prettyPrint === true\n });\n return builder.build(xml);\n }\n };\n}\n"
],
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAmC,IAAnC;;;ACAuB,IAAvB;AAaO,SAAS,QAAQ,CAAC,KAAsB;AAAA,EAC7C,IAAI,OAAO,QAAQ;AAAA,IAAU,OAAO;AAAA,EAEpC,OAAO,0BAAW,OAChB,IACG,QAAQ,MAAM,KAAK,EACnB,QAAQ,YAAW,GAAG,EACtB,QAAQ,+hCAA6hC,EAAE,EACviC,YAAY,CACjB;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ADVF,IAAM,cAAiD,oBAAgB;AAGvE,IAAM,QAA4C,cAAU;AAAA;AA+BrD,MAAM,QAAQ;AAAA,EAEX;AAAA,EAEA,eAAe,IAAI;AAAA,EAEnB;AAAA,EAEA;AAAA,EAEA,WAA0B,CAAC;AAAA,EAOnC,WAAW,GAAG;AAAA,IAyCZ,KAAK,aAAa;AAAA,IAIlB,KAAK,eAAe,IAAI;AAAA,IACxB,WAAW,KAAM,qBAAiB,gBAAgB,CAAC,GAAI;AAAA,MACrD,KAAK,aAAa,IAAI,GAAG,IAAI,OAAO,GAAG,GAAG,CAAC;AAAA,IAC7C;AAAA,IAKA,KAAK,OAAO;AAAA,IACZ,KAAK,oBAAoB;AAAA,IAGzB,KAAK,WAAW,CAAC;AAAA;AAAA,EAmBnB,eAAe,CAAC,MAAqB;AAAA,IACnC,IAAI,KAAK;AAAA,MAAY;AAAA,IAErB,MAAM,aAAa,IAAI;AAAA,IACvB,KAAK,aAAa;AAAA,IAElB,MAAM,WAAW,IAAI;AAAA,IAGrB,MAAM,uBAAuB,CAAC,GAAW,GAAW,MAAuB;AAAA,MACzE,OACE,MAAM,WACN,MAAM,aACN,MAAM,aACN,MAAM,SACN,MAAM,qBACN,MAAM,iBACN,MAAM,kBACN,MAAM,sBACN,MAAM,cACN,MAAM,aACN,MAAM,WACN,MAAM,gBACN,MAAM,cACN,MAAM,qBACN,MAAM,eACN,MAAM;AAAA;AAAA,IAKV,MAAM,aAAa,CAAC,OAAgC,GAAW,IAAY,SAAiB,WAAmB;AAAA,MAC7G,IAAI,CAAC,SAAS;AAAA,QACZ,KAAK,SAAS,KAAK,2BAA2B,uBAAuB,KAAK,OAAO,QAAQ;AAAA,QACzF;AAAA,MACF;AAAA,MAEA,IAAI,SAAS,WAAW,IAAI,EAAE;AAAA,MAC9B,IAAI,CAAC,QAAQ;AAAA,QACX,SAAS;AAAA,UACP,SAAS,IAAI;AAAA,UACb,WAAW,IAAI;AAAA,UACf,gBAAgB,IAAI;AAAA,UACpB,cAAc,IAAI;AAAA,QACpB;AAAA,QACA,WAAW,IAAI,IAAI,MAAM;AAAA,MAC3B;AAAA