mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Both stack parents (#837, #842) are on main, so the voice geometry branch now builds on main directly. Conflict resolutions keep main's behaviour and add the geometry work on top: osm_query gating on every provider, restored admin identity for Nominatim outlines, merged analyst follow-up memory, and the cancelled Globe View reset instruction ahead of the generic cancellation.
427 lines
11 KiB
JavaScript
427 lines
11 KiB
JavaScript
/**
|
|
* The OpenStreetMap feature kinds voice can search for ("hospitals",
|
|
* "petrol stations", "police"), each with the exact tags it stands for.
|
|
*
|
|
* The server compiles Overpass queries only from these ids: a request names a
|
|
* preset, never a tag, a regex or query text. Pure data; the browser and the
|
|
* server both import it.
|
|
*
|
|
* @module data/osmPresets
|
|
*/
|
|
|
|
/**
|
|
* The noun is the model's data, not UI copy: it stays English.
|
|
* id → { noun (plural, spoken), terms (other ways to say it), tags: [[key, value]] }.
|
|
* A feature matches a preset when it carries ANY of the preset's tags.
|
|
*/
|
|
const PRESETS = {
|
|
hospital: {
|
|
noun: 'hospitals',
|
|
terms: ['hospital', 'er', 'emergency room', 'medical center'],
|
|
tags: [['amenity', 'hospital']],
|
|
},
|
|
clinic: {
|
|
noun: 'clinics',
|
|
terms: ['clinic', 'health center', 'health post', 'medical clinic'],
|
|
tags: [['amenity', 'clinic']],
|
|
},
|
|
doctors: {
|
|
noun: 'doctors',
|
|
terms: ['doctor', "doctor's office", 'gp', 'physician'],
|
|
tags: [['amenity', 'doctors']],
|
|
},
|
|
dentist: {
|
|
noun: 'dentists',
|
|
terms: ['dentist', 'dental clinic'],
|
|
tags: [['amenity', 'dentist']],
|
|
},
|
|
pharmacy: {
|
|
noun: 'pharmacies',
|
|
terms: ['pharmacy', 'chemist', 'drugstore', 'drug store'],
|
|
tags: [['amenity', 'pharmacy']],
|
|
},
|
|
school: {
|
|
noun: 'schools',
|
|
terms: ['school', 'primary school', 'secondary school', 'high school'],
|
|
tags: [['amenity', 'school']],
|
|
},
|
|
kindergarten: {
|
|
noun: 'kindergartens',
|
|
terms: ['kindergarten', 'preschool', 'nursery school'],
|
|
tags: [['amenity', 'kindergarten']],
|
|
},
|
|
university: {
|
|
noun: 'universities',
|
|
terms: ['university', 'campus'],
|
|
tags: [['amenity', 'university']],
|
|
},
|
|
college: {
|
|
noun: 'colleges',
|
|
terms: ['college'],
|
|
tags: [['amenity', 'college']],
|
|
},
|
|
library: {
|
|
noun: 'libraries',
|
|
terms: ['library'],
|
|
tags: [['amenity', 'library']],
|
|
},
|
|
police: {
|
|
noun: 'police stations',
|
|
terms: ['police', 'police station', 'police post'],
|
|
tags: [['amenity', 'police']],
|
|
},
|
|
fire_station: {
|
|
noun: 'fire stations',
|
|
terms: ['fire station', 'fire department', 'firehouse'],
|
|
tags: [['amenity', 'fire_station']],
|
|
},
|
|
prison: {
|
|
noun: 'prisons',
|
|
terms: ['prison', 'jail', 'correctional facility'],
|
|
tags: [['amenity', 'prison']],
|
|
},
|
|
courthouse: {
|
|
noun: 'courthouses',
|
|
terms: ['courthouse', 'court'],
|
|
tags: [['amenity', 'courthouse']],
|
|
},
|
|
townhall: {
|
|
noun: 'town halls',
|
|
terms: ['town hall', 'city hall'],
|
|
tags: [['amenity', 'townhall']],
|
|
},
|
|
embassy: {
|
|
noun: 'embassies',
|
|
terms: ['embassy', 'consulate'],
|
|
tags: [
|
|
['office', 'diplomatic'],
|
|
['amenity', 'embassy'],
|
|
],
|
|
},
|
|
post_office: {
|
|
noun: 'post offices',
|
|
terms: ['post office'],
|
|
tags: [['amenity', 'post_office']],
|
|
},
|
|
bank: { noun: 'banks', terms: ['bank'], tags: [['amenity', 'bank']] },
|
|
atm: {
|
|
noun: 'ATMs',
|
|
terms: ['atm', 'cash machine', 'cashpoint'],
|
|
tags: [['amenity', 'atm']],
|
|
},
|
|
fuel: {
|
|
noun: 'fuel stations',
|
|
terms: [
|
|
'gas station',
|
|
'petrol station',
|
|
'fuel station',
|
|
'filling station',
|
|
'gas',
|
|
],
|
|
tags: [['amenity', 'fuel']],
|
|
},
|
|
charging_station: {
|
|
noun: 'EV charging stations',
|
|
terms: ['charging station', 'ev charger', 'ev charging', 'car charger'],
|
|
tags: [['amenity', 'charging_station']],
|
|
},
|
|
parking: {
|
|
noun: 'car parks',
|
|
terms: ['parking', 'parking lot', 'car park', 'parking garage'],
|
|
tags: [['amenity', 'parking']],
|
|
},
|
|
restaurant: {
|
|
noun: 'restaurants',
|
|
terms: ['restaurant'],
|
|
tags: [['amenity', 'restaurant']],
|
|
},
|
|
cafe: {
|
|
noun: 'cafés',
|
|
terms: ['cafe', 'café', 'coffee shop', 'coffee'],
|
|
tags: [['amenity', 'cafe']],
|
|
},
|
|
fast_food: {
|
|
noun: 'fast-food places',
|
|
terms: ['fast food', 'fast-food'],
|
|
tags: [['amenity', 'fast_food']],
|
|
},
|
|
bar: {
|
|
noun: 'bars',
|
|
terms: ['bar', 'pub'],
|
|
tags: [
|
|
['amenity', 'bar'],
|
|
['amenity', 'pub'],
|
|
],
|
|
},
|
|
place_of_worship: {
|
|
noun: 'places of worship',
|
|
terms: [
|
|
'place of worship',
|
|
'church',
|
|
'mosque',
|
|
'temple',
|
|
'synagogue',
|
|
'gurdwara',
|
|
],
|
|
tags: [['amenity', 'place_of_worship']],
|
|
},
|
|
marketplace: {
|
|
noun: 'markets',
|
|
terms: ['market', 'marketplace', 'bazaar'],
|
|
tags: [['amenity', 'marketplace']],
|
|
},
|
|
supermarket: {
|
|
noun: 'supermarkets',
|
|
terms: ['supermarket', 'grocery store', 'grocery'],
|
|
tags: [['shop', 'supermarket']],
|
|
},
|
|
mall: {
|
|
noun: 'shopping malls',
|
|
terms: ['mall', 'shopping mall', 'shopping center', 'shopping centre'],
|
|
tags: [['shop', 'mall']],
|
|
},
|
|
convenience: {
|
|
noun: 'convenience stores',
|
|
terms: ['convenience store', 'corner shop'],
|
|
tags: [['shop', 'convenience']],
|
|
},
|
|
hotel: { noun: 'hotels', terms: ['hotel'], tags: [['tourism', 'hotel']] },
|
|
hostel: {
|
|
noun: 'hostels',
|
|
terms: ['hostel'],
|
|
tags: [['tourism', 'hostel']],
|
|
},
|
|
museum: {
|
|
noun: 'museums',
|
|
terms: ['museum'],
|
|
tags: [['tourism', 'museum']],
|
|
},
|
|
attraction: {
|
|
noun: 'tourist attractions',
|
|
terms: ['tourist attraction', 'attraction', 'sight', 'sights'],
|
|
tags: [['tourism', 'attraction']],
|
|
},
|
|
viewpoint: {
|
|
noun: 'viewpoints',
|
|
terms: ['viewpoint', 'lookout', 'scenic view'],
|
|
tags: [['tourism', 'viewpoint']],
|
|
},
|
|
zoo: { noun: 'zoos', terms: ['zoo'], tags: [['tourism', 'zoo']] },
|
|
cinema: {
|
|
noun: 'cinemas',
|
|
terms: ['cinema', 'movie theater', 'movie theatre'],
|
|
tags: [['amenity', 'cinema']],
|
|
},
|
|
theatre: {
|
|
noun: 'theatres',
|
|
terms: ['theatre', 'theater', 'playhouse'],
|
|
tags: [['amenity', 'theatre']],
|
|
},
|
|
stadium: {
|
|
noun: 'stadiums',
|
|
terms: ['stadium', 'arena'],
|
|
tags: [['leisure', 'stadium']],
|
|
},
|
|
park: { noun: 'parks', terms: ['park'], tags: [['leisure', 'park']] },
|
|
playground: {
|
|
noun: 'playgrounds',
|
|
terms: ['playground'],
|
|
tags: [['leisure', 'playground']],
|
|
},
|
|
sports_centre: {
|
|
noun: 'sports centres',
|
|
terms: ['sports center', 'sports centre', 'gym', 'leisure center'],
|
|
tags: [
|
|
['leisure', 'sports_centre'],
|
|
['leisure', 'fitness_centre'],
|
|
],
|
|
},
|
|
cemetery: {
|
|
noun: 'cemeteries',
|
|
terms: ['cemetery', 'graveyard'],
|
|
tags: [
|
|
['landuse', 'cemetery'],
|
|
['amenity', 'grave_yard'],
|
|
],
|
|
},
|
|
toilets: {
|
|
noun: 'public toilets',
|
|
terms: ['toilet', 'toilets', 'restroom', 'public toilet', 'bathroom'],
|
|
tags: [['amenity', 'toilets']],
|
|
},
|
|
drinking_water: {
|
|
noun: 'drinking water points',
|
|
terms: ['drinking water', 'water fountain', 'water point'],
|
|
tags: [['amenity', 'drinking_water']],
|
|
},
|
|
shelter: {
|
|
noun: 'shelters',
|
|
terms: ['shelter', 'emergency shelter'],
|
|
tags: [['amenity', 'shelter']],
|
|
},
|
|
airport: {
|
|
noun: 'airports',
|
|
terms: ['airport', 'airfield', 'aerodrome', 'airstrip'],
|
|
tags: [['aeroway', 'aerodrome']],
|
|
},
|
|
helipad: {
|
|
noun: 'helipads',
|
|
terms: ['helipad', 'heliport'],
|
|
tags: [
|
|
['aeroway', 'helipad'],
|
|
['aeroway', 'heliport'],
|
|
],
|
|
},
|
|
train_station: {
|
|
noun: 'train stations',
|
|
terms: ['train station', 'railway station', 'rail station'],
|
|
tags: [['railway', 'station']],
|
|
},
|
|
bus_station: {
|
|
noun: 'bus stations',
|
|
terms: ['bus station', 'bus terminal', 'bus depot'],
|
|
tags: [['amenity', 'bus_station']],
|
|
},
|
|
ferry_terminal: {
|
|
noun: 'ferry terminals',
|
|
terms: ['ferry terminal', 'ferry'],
|
|
tags: [['amenity', 'ferry_terminal']],
|
|
},
|
|
harbour: {
|
|
noun: 'harbours',
|
|
terms: ['harbour', 'harbor', 'port', 'marina'],
|
|
tags: [
|
|
['harbour', 'yes'],
|
|
['leisure', 'marina'],
|
|
['landuse', 'port'],
|
|
],
|
|
},
|
|
bridge: {
|
|
noun: 'bridges',
|
|
terms: ['bridge'],
|
|
tags: [['man_made', 'bridge']],
|
|
},
|
|
lighthouse: {
|
|
noun: 'lighthouses',
|
|
terms: ['lighthouse'],
|
|
tags: [['man_made', 'lighthouse']],
|
|
},
|
|
water_tower: {
|
|
noun: 'water towers',
|
|
terms: ['water tower'],
|
|
tags: [['man_made', 'water_tower']],
|
|
},
|
|
power_plant: {
|
|
noun: 'power plants',
|
|
terms: ['power plant', 'power station', 'generating station'],
|
|
tags: [['power', 'plant']],
|
|
},
|
|
substation: {
|
|
noun: 'substations',
|
|
terms: ['substation', 'electrical substation'],
|
|
tags: [['power', 'substation']],
|
|
},
|
|
wind_turbine: {
|
|
noun: 'wind turbines',
|
|
terms: ['wind turbine', 'windmill'],
|
|
tags: [['generator:source', 'wind']],
|
|
},
|
|
dam: { noun: 'dams', terms: ['dam'], tags: [['waterway', 'dam']] },
|
|
military: {
|
|
noun: 'military sites',
|
|
terms: ['military base', 'military site', 'base', 'barracks'],
|
|
tags: [
|
|
['landuse', 'military'],
|
|
['military', 'base'],
|
|
['military', 'barracks'],
|
|
],
|
|
},
|
|
peak: {
|
|
noun: 'peaks',
|
|
terms: ['peak', 'mountain peak', 'summit', 'mountain'],
|
|
tags: [['natural', 'peak']],
|
|
},
|
|
waterfall: {
|
|
noun: 'waterfalls',
|
|
terms: ['waterfall', 'falls'],
|
|
tags: [['waterway', 'waterfall']],
|
|
},
|
|
};
|
|
|
|
/** Frozen preset table. */
|
|
export const OSM_PRESETS = Object.freeze(
|
|
Object.fromEntries(
|
|
Object.entries(PRESETS).map(([id, preset]) => [
|
|
id,
|
|
Object.freeze({
|
|
id,
|
|
label: preset.noun,
|
|
terms: Object.freeze([...preset.terms]),
|
|
tags: Object.freeze(
|
|
preset.tags.map((pair) => Object.freeze([...pair])),
|
|
),
|
|
}),
|
|
]),
|
|
),
|
|
);
|
|
|
|
/** Every preset id, for tool enums. */
|
|
export const OSM_PRESET_IDS = Object.freeze(Object.keys(OSM_PRESETS));
|
|
|
|
const normalize = (text) =>
|
|
String(text || '')
|
|
.toLowerCase()
|
|
.normalize('NFD')
|
|
.replace(/\p{M}/gu, '')
|
|
.replace(/[^a-z0-9 ]+/g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
|
|
/** Naive singular: "pharmacies" → "pharmacy", "hospitals" → "hospital". */
|
|
function singular(word) {
|
|
if (word.endsWith('ies')) return `${word.slice(0, -3)}y`;
|
|
if (/(ches|shes|sses|xes)$/.test(word)) return word.slice(0, -2);
|
|
if (word.endsWith('s') && !word.endsWith('ss')) return word.slice(0, -1);
|
|
return word;
|
|
}
|
|
const singularPhrase = (text) => text.split(' ').map(singular).join(' ');
|
|
|
|
const TERM_INDEX = (() => {
|
|
const index = new Map();
|
|
for (const preset of Object.values(OSM_PRESETS)) {
|
|
for (const term of [
|
|
preset.id.replace(/_/g, ' '),
|
|
singularPhrase(normalize(preset.label)),
|
|
...preset.terms,
|
|
]) {
|
|
const key = singularPhrase(normalize(term));
|
|
if (key && !index.has(key)) index.set(key, preset.id);
|
|
}
|
|
}
|
|
return index;
|
|
})();
|
|
|
|
/**
|
|
* The preset a spoken phrase names ("all the hospitals" → hospital), or null.
|
|
* An exact preset id wins; otherwise the longest known term found in the
|
|
* phrase.
|
|
* @param {string} phrase
|
|
* @returns {object|null} A preset.
|
|
*/
|
|
export function matchOsmPreset(phrase) {
|
|
const raw = String(phrase || '').trim();
|
|
if (Object.hasOwn(OSM_PRESETS, raw)) return OSM_PRESETS[raw];
|
|
const text = singularPhrase(
|
|
normalize(raw).replace(/^(all|every|the|any)( the)? /, ''),
|
|
);
|
|
if (!text) return null;
|
|
if (TERM_INDEX.has(text)) return OSM_PRESETS[TERM_INDEX.get(text)];
|
|
let best = null;
|
|
for (const [term, id] of TERM_INDEX) {
|
|
if (term.length < 3) continue;
|
|
if (!new RegExp(`(^| )${term}( |$)`).test(text)) continue;
|
|
if (!best || term.length > best.term.length) best = { term, id };
|
|
}
|
|
return best ? OSM_PRESETS[best.id] : null;
|
|
}
|