2026-10-03 13:33:50 +02:00
|
|
|
import { fold } from './text';
|
|
|
|
|
import type { Place } from './types';
|
|
|
|
|
|
|
|
|
|
export interface Labels {
|
|
|
|
|
byLabel: Map<string, Place>;
|
|
|
|
|
labelOf: Map<string, string>;
|
|
|
|
|
list: string[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Unique human-readable label per place, usable as a datalist value. */
|
|
|
|
|
export function buildLabels(places: Place[]): Labels {
|
|
|
|
|
const byLabel = new Map<string, Place>();
|
|
|
|
|
const labelOf = new Map<string, string>();
|
|
|
|
|
for (const p of places) {
|
|
|
|
|
if (!p.name) continue;
|
|
|
|
|
const addr = [p.tags['addr:street'], p.tags['addr:housenumber']].filter(Boolean).join(' ');
|
|
|
|
|
let label = addr ? `${p.name} — ${addr}` : p.name;
|
|
|
|
|
if (byLabel.has(label)) label = `${label} (${p.id})`;
|
|
|
|
|
byLabel.set(label, p);
|
|
|
|
|
labelOf.set(p.id, label);
|
|
|
|
|
}
|
|
|
|
|
return { byLabel, labelOf, list: [...byLabel.keys()] };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Exact label, else unique folded match on name prefix. */
|
|
|
|
|
export function resolveLabel(labels: Labels, text: string): Place | null {
|
|
|
|
|
const exact = labels.byLabel.get(text.trim());
|
|
|
|
|
if (exact) return exact;
|
|
|
|
|
const q = fold(text.trim());
|
|
|
|
|
if (!q) return null;
|
|
|
|
|
const hits = [...labels.byLabel.entries()].filter(([l]) => fold(l).startsWith(q));
|
2026-10-03 19:14:41 +02:00
|
|
|
if (hits.length === 1) return hits[0]![1];
|
|
|
|
|
// several prefix matches (e.g. a stadium and the stops named after it): an exact name match wins if it is unique
|
|
|
|
|
const exactName = hits.filter(([, pl]) => fold(pl.name ?? '') === q && pl.cat !== 'transit');
|
|
|
|
|
return exactName.length === 1 ? exactName[0]![1] : null;
|
2026-10-03 13:33:50 +02:00
|
|
|
}
|