mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Merge feat/voice-geometry onto main
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.
This commit is contained in:
commit
ecaddd838b
102 files changed
+12925
-1006
No files matched your search
@@ -0,0 +1,426 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in new issue
Block a user