// This file is partially generated by scripts/generate-poi-icon-data.cjs // Do not edit the POI_ICON_SVGS section. The helper functions and CATEGORY_ALIASES below are hand-maintained. /** * Map of lowercased icon name → raw SVG string. * Keys correspond to filenames in images/icons/ (without .svg extension, lowercased). */ export const POI_ICON_SVGS: Record = { "accessible restroom": '\n\n\n\n', "aerialway station": '\n\n', airport: '\n\n', "amusement park": '\n\n\n\n\n', "automotive dealer": '\n\n\n\n', bank: '\n\n', beach: '\n\n\n', beauty: '\n\n\n\n\n\n', building: '\n\n\n', "bus stop": '\n\n\n\n', cafe: '\n\n\n', campsite: '\n\n', "car rental": '\n\n\n', "car wash": '\n\n\n\n\n', "cash dispenser": '\n\n\n\n\n\n', casino: '\n\n\n', checkpoint: '\n\n\n\n\n\n', cinema: '\n\n\n\n', company: '\n\n', "concert hall": '\n\n\n\n', "convenience store": '\n\n\n', "courier drop box": '\n\n', court: '\n\n', dentist: '\n\n', doctor: '\n\n', "emergency room": '\n\n\n', "ev charger": '\n\n', exchange: '\n\n\n\n\n\n', "fast food": '\n\n\n', ferry: '\n\n\n\n', firestation: '\n\n', fuel: '\n\n', generic: '\n\n', "geographical feature": '\n\n\n', "government office": '\n\n', helipad: '\n\n', "hospital - polyclinic": '\n\n', "hotel - motel": '\n\n\n', "ice skating ring": '\n\n', "leisure centre": '\n\n\n\n', library: '\n\n\n\n', market: '\n\n\n\n\n\n\n', military: '\n\n', "mountain pass": '\n\n', museum: '\n\n\n\n\n\n\n', ngo: '\n\n', nightlife: '\n\n\n', park: '\n\n\n', "parking garage": '\n\n\n', pharmacy: '\n\n\n\n', "place of worship": '\n\n\n', "police station": '\n\n', prison: '\n\n\n', "pub - bar": '\n\n\n', "rent a car parking": '\n\n\n\n', "repair facility": '\n\n', "rest area": '\n\n\n', restaurant: '\n\n\n', scenic: '\n\n', school: '\n\n\n', "service area": '\n\n\n', shop: '\n\n', "sports center": '\n\n', stadium: '\n\n\n\n\n\n\n', supermarket: '\n\n', subway: '\n\n\n\n\n\n\n', "swimming pool": '\n\n\n\n', "taxi stand": '\n\n', "tennis court": '\n\n\n\n\n', theatre: '\n\n\n', tollgate: '\n\n\n', "tourist attraction": '\n\n', "tourist attraction - korea": '\n\n\n\n', "train station": '\n\n', tram: '\n\n', "transport access": '\n\n', "truck dealer": '\n\n\n\n\n', "truck stop": '\n\n\n\n\n\n', veterinarian: '\n\n\n\n\n\n', "weight station": '\n\n\n', "welfare orginization": '\n\n\n', winery: '\n\n\n\n\n\n\n\n\n', zoo: '\n\n\n', }; // ── Category aliases (hand-maintained) ────────────────────────────────────── /** * Maps common category names to icon keys that don't match 1:1. * All keys and values must be lowercase. */ const CATEGORY_ALIASES: Record = { // Shopping shopping: "shop", "shopping mall": "shop", "shopping center": "shop", "shopping centre": "shop", retail: "shop", store: "shop", boutique: "shop", outlet: "shop", "department store": "shop", mall: "shop", marketplace: "market", "flea market": "market", "farmers market": "market", // Transit — general "transit hub": "transport access", transit: "transport access", "public transport": "transport access", transportation: "transport access", "transport hub": "transport access", commuter: "transport access", "commuter station": "transport access", // Train "railway station": "train station", railway: "train station", rail: "train station", "rail station": "train station", intercity: "train station", // Subway / Metro "metro station": "subway", metro: "subway", "subway station": "subway", underground: "subway", tube: "subway", "tube station": "subway", "rapid transit": "subway", // Tram trolley: "tram", streetcar: "tram", "light rail": "tram", "light railway": "tram", tramway: "tram", // Taxi taxi: "taxi stand", cab: "taxi stand", rideshare: "taxi stand", "ride hailing": "taxi stand", "taxi rank": "taxi stand", minicab: "taxi stand", // Aerialway "cable car": "aerialway station", gondola: "aerialway station", "ski lift": "aerialway station", "aerial tramway": "aerialway station", chairlift: "aerialway station", funicular: "aerialway station", ropeway: "aerialway station", // Bus "bus station": "bus stop", "bus terminal": "bus stop", "bus depot": "bus stop", coach: "bus stop", "coach station": "bus stop", // Ferry "ferry terminal": "ferry", "boat terminal": "ferry", port: "ferry", harbour: "ferry", harbor: "ferry", // Bar & Nightlife "bar & nightlife": "nightlife", "bar and nightlife": "nightlife", "bars & nightlife": "nightlife", "bars and nightlife": "nightlife", nightclub: "nightlife", lounge: "nightlife", disco: "nightlife", "dance club": "nightlife", bar: "pub - bar", pub: "pub - bar", tavern: "pub - bar", "cocktail bar": "pub - bar", // Landmark & Historic landmark: "tourist attraction", landmarks: "tourist attraction", monument: "tourist attraction", "historic site": "tourist attraction", historic: "tourist attraction", "historical site": "tourist attraction", historical: "tourist attraction", heritage: "tourist attraction", "heritage site": "tourist attraction", sightseeing: "tourist attraction", "point of interest": "tourist attraction", poi: "tourist attraction", attraction: "tourist attraction", // Religious "religious site": "place of worship", religious: "place of worship", worship: "place of worship", church: "place of worship", mosque: "place of worship", temple: "place of worship", synagogue: "place of worship", cathedral: "place of worship", chapel: "place of worship", shrine: "place of worship", monastery: "place of worship", pagoda: "place of worship", // Fuel "gas station": "fuel", "petrol station": "fuel", petrol: "fuel", gas: "fuel", "filling station": "fuel", "fuel station": "fuel", // Accommodation hotel: "hotel - motel", motel: "hotel - motel", accommodation: "hotel - motel", lodging: "hotel - motel", hostel: "hotel - motel", inn: "hotel - motel", "bed and breakfast": "hotel - motel", "b&b": "hotel - motel", guesthouse: "hotel - motel", resort: "hotel - motel", // Medical hospital: "hospital - polyclinic", polyclinic: "hospital - polyclinic", clinic: "hospital - polyclinic", "medical center": "hospital - polyclinic", "medical centre": "hospital - polyclinic", "health center": "hospital - polyclinic", "health centre": "hospital - polyclinic", "urgent care": "hospital - polyclinic", // Pharmacy drugstore: "pharmacy", chemist: "pharmacy", // Finance atm: "cash dispenser", "cash machine": "cash dispenser", "money exchange": "exchange", "currency exchange": "exchange", // EV "ev charging": "ev charger", "ev charging station": "ev charger", "electric vehicle": "ev charger", "charging station": "ev charger", charger: "ev charger", // Arts & Entertainment entertainment: "theatre", "arts venue": "theatre", arts: "theatre", "performing arts": "theatre", theater: "theatre", "concert venue": "concert hall", concert: "concert hall", "live music": "concert hall", gallery: "museum", "art gallery": "museum", "art museum": "museum", exhibition: "museum", "movie theater": "cinema", "movie theatre": "cinema", movies: "cinema", multiplex: "cinema", // Food & Grocery grocery: "supermarket", "grocery store": "supermarket", groceries: "supermarket", dining: "restaurant", food: "restaurant", cafe: "restaurant", coffee: "restaurant", "coffee shop": "restaurant", bistro: "restaurant", diner: "restaurant", eatery: "restaurant", cafeteria: "restaurant", // Education university: "school", college: "school", academy: "school", institute: "school", education: "school", // Sports & Fitness gym: "sports center", fitness: "sports center", "fitness center": "sports center", "fitness centre": "sports center", sports: "sports center", "sports club": "sports center", // Ice "ice rink": "ice skating ring", "skating rink": "ice skating ring", // Government government: "government office", "government building": "government office", "city hall": "government office", "town hall": "government office", municipality: "government office", "civic center": "government office", "civic centre": "government office", embassy: "government office", consulate: "government office", courthouse: "court", // Emergency & Services "fire station": "firestation", "fire department": "firestation", "fire brigade": "firestation", police: "police station", "police department": "police station", "post office": "courier drop box", postal: "courier drop box", // Travel "rest stop": "rest area", "service station": "service area", // Drinks wine: "winery", vineyard: "winery", brewery: "winery", // Recreation aquarium: "zoo", "wildlife park": "zoo", safari: "zoo", "theme park": "amusement park", fairground: "amusement park", "water park": "amusement park", // Parking toll: "tollgate", parking: "parking garage", "parking lot": "parking garage", "car park": "parking garage", "multi storey car park": "parking garage", "park and ride": "parking garage", // Organization ngo: "ngo", nonprofit: "ngo", charity: "welfare orginization", // Beach & Outdoors seaside: "beach", coast: "beach", shore: "beach", // Camping "caravan park": "campsite", "rv park": "campsite", camping: "campsite", // Direct matches (aliases that match icon keys for completeness) "fast food": "fast food", "car wash": "car wash", "car rental": "car rental", "train station": "train station", "bus stop": "bus stop", }; // Pre-computed sorted icon keys (longest first for best substring match) const ICON_KEYS = Object.keys(POI_ICON_SVGS).sort((a, b) => b.length - a.length); const ALIAS_KEYS = Object.keys(CATEGORY_ALIASES).sort((a, b) => b.length - a.length); /** * Resolve a category string to a POI icon key using multi-level matching: * 1. Exact match — "restaurant" → "restaurant" * 2. Alias match — "gas station" → "fuel" * 3. Contains match — "Italian Restaurant" contains "restaurant" → match * 4. Word-level alias — "EV Charging Point" contains "ev charging" alias → "ev charger" * * Returns the icon key or null if no match is found. */ export function resolveIconKey(category: string): string | null { const normalized = category.toLowerCase().trim(); // 1. Direct exact match if (POI_ICON_SVGS[normalized]) return normalized; // 2. Alias exact match const alias = CATEGORY_ALIASES[normalized]; if (alias && POI_ICON_SVGS[alias]) return alias; // 3. Contains match — check if any icon key is a substring of the category // (longest key first to prefer "tourist attraction" over "park") for (const key of ICON_KEYS) { if (key === "generic") continue; if (normalized.includes(key)) return key; } // 4. Word-level alias — check if any alias key is a substring of the category for (const aliasKey of ALIAS_KEYS) { if (normalized.includes(aliasKey)) { const target = CATEGORY_ALIASES[aliasKey]; if (target && POI_ICON_SVGS[target]) return target; } } return null; } // ── SVG path extraction ──────────────────────────────────────────────────── export interface SvgPathData { d: string; fillRule: "nonzero" | "evenodd"; } /** * Convert a circle element to an SVG arc path string. */ function circleToPath(cx: number, cy: number, r: number): string { return ( `M ${cx - r} ${cy} ` + `A ${r} ${r} 0 1 0 ${cx + r} ${cy} ` + `A ${r} ${r} 0 1 0 ${cx - r} ${cy} Z` ); } /** * Parse SVG content and extract renderable path data. * Handles and elements. */ export function extractSvgPaths(svgContent: string): SvgPathData[] { const paths: SvgPathData[] = []; // Match elements (self-closing) const pathRegex = /]*?)\s*\/>/g; let match; while ((match = pathRegex.exec(svgContent)) !== null) { const attrs = match[1]; const dMatch = attrs.match(/d="([^"]*)"/); if (dMatch) { const fillRuleMatch = attrs.match(/fill-rule="([^"]*)"/); paths.push({ d: dMatch[1], fillRule: fillRuleMatch?.[1] === "evenodd" ? "evenodd" : "nonzero", }); } } // Match elements (e.g., Generic.svg) const circleRegex = /]*?)\s*\/>/g; while ((match = circleRegex.exec(svgContent)) !== null) { const attrs = match[1]; const cxMatch = attrs.match(/cx="([^"]*)"/); const cyMatch = attrs.match(/cy="([^"]*)"/); const rMatch = attrs.match(/r="([^"]*)"/); if (cxMatch && cyMatch && rMatch) { paths.push({ d: circleToPath(parseFloat(cxMatch[1]), parseFloat(cyMatch[1]), parseFloat(rMatch[1])), fillRule: "nonzero", }); } } return paths; }