mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
feat(roadtrip): answer the corridor search from the place index
Search along the route went to Overpass and only to Overpass, on the one path where that hurts most: one search fans out over sixteen boxes, and each of those races four public mirrors. The index answers the map's explore pills in milliseconds where those mirrors take seconds, and the corridor was the reason the index exists. Two independent things kept it out. The corridor sends its categories as a comma-separated list, and the index branch looked the whole string up as one key, so it missed every time. And the four kinds a drive needs, fuel, charging, rest areas and campsites, had no Overture terms mapped at all. They do now: gas_station and fueling_station, ev_charging_station, rest_stop, and campground with rv_park. The branch is list-aware the way searchOverpassPois already was, and it labels each hit with the category that produced it rather than with the list that was asked for, because the client colours and groups its markers by that field and "fuel,charging" is not a category. It answers all-or-nothing: a category the index has no terms for sends the whole query to Overpass, since answering the half it knows would silently drop the rest while the panel still reported a finished search. The caller's limit is passed through instead of a hardcoded 50, so a mixed search gets the same per-category budget the Overpass path spends. The other outbound path of this addon changes with it. Importing a shared Google route geocoded its waypoints by calling searchNominatim directly, which walked past geocodeQuery and its index lookup. Through geocodeQuery an index hit costs no throttle slot at all, and what the index does not know still falls through on the background lane. A place answered from the index also carries its chain and that chain's Wikidata item, so a branch gets its logo on the pin here exactly as it does on the Overpass path. Sockets stay an OSM field: a charging station answered from the index reports "not stated" rather than claiming it offers nothing.
This commit is contained in:
@@ -820,24 +820,45 @@ export class MapsService {
|
||||
category: string,
|
||||
bbox: { south: number; west: number; north: number; east: number },
|
||||
lang?: string,
|
||||
limit = 60,
|
||||
): Promise<PoiSearchResult> {
|
||||
const terms = POI_CATEGORY_TO_TREK[category];
|
||||
if (this.trekPlacesEnabled() && terms) {
|
||||
// One or several categories, the way searchOverpassPois reads them. The
|
||||
// corridor search sends a comma-separated list, and looking the whole string
|
||||
// up as one key missed every time — so every corridor query fell through to
|
||||
// Overpass, on the one path where that hurts most: a single search fans out
|
||||
// over sixteen boxes, and each of those races four public mirrors.
|
||||
const wanted = parsePoiCategories(category);
|
||||
// Which category each Overture term belongs to, so a hit can be labelled
|
||||
// with the category that actually produced it rather than with the whole
|
||||
// list. The client colours and groups its markers by that field.
|
||||
const categoryOfTerm = new Map<string, string>();
|
||||
for (const key of wanted) {
|
||||
for (const term of POI_CATEGORY_TO_TREK[key] ?? []) categoryOfTerm.set(term, key);
|
||||
}
|
||||
// All or nothing: a category the index has no terms for has to be answered
|
||||
// by Overpass, and a mixed answer would silently drop it.
|
||||
const indexKnowsAll = wanted.length > 0 && wanted.every(key => POI_CATEGORY_TO_TREK[key]?.length);
|
||||
const terms = [...categoryOfTerm.keys()];
|
||||
if (this.trekPlacesEnabled() && indexKnowsAll) {
|
||||
try {
|
||||
const lat = (bbox.south + bbox.north) / 2;
|
||||
const lng = (bbox.west + bbox.east) / 2;
|
||||
// Half the diagonal, so the circle covers the viewport corners rather
|
||||
// than leaving the edges of the map empty.
|
||||
const wanted = Math.round(
|
||||
const reach = Math.round(
|
||||
Math.hypot(
|
||||
(bbox.north - bbox.south) * 111_320,
|
||||
(bbox.east - bbox.west) * 111_320 * Math.cos((lat * Math.PI) / 180),
|
||||
) / 2,
|
||||
);
|
||||
const radius = Math.min(20000, Math.max(300, wanted));
|
||||
const radius = Math.min(20000, Math.max(300, reach));
|
||||
// The same budget the Overpass path spends: per category, capped, so a
|
||||
// mixed search does not spend the whole allowance on whichever kind
|
||||
// happens to be densest.
|
||||
const cap = Math.min(limit * wanted.length, POI_RESULT_CAP);
|
||||
const found = await trekPlacesNearby(lat, lng, {
|
||||
radius,
|
||||
limit: 50,
|
||||
limit: cap,
|
||||
category: terms.join(','),
|
||||
});
|
||||
if (found.length > 0) {
|
||||
@@ -859,8 +880,11 @@ export class MapsService {
|
||||
name: p.name,
|
||||
lat: p.lat,
|
||||
lng: p.lng,
|
||||
category,
|
||||
poi_type: p.category ?? category,
|
||||
// The category that produced the hit, not the list that was
|
||||
// asked for: a mixed search must not label a petrol station as
|
||||
// "fuel,charging,restaurant".
|
||||
category: categoryOfTerm.get(p.category ?? '') ?? wanted[0],
|
||||
poi_type: p.category ?? wanted[0],
|
||||
address: p.address?.freeform ?? null,
|
||||
website: p.contact?.website ?? null,
|
||||
phone: p.contact?.phone ?? null,
|
||||
@@ -880,17 +904,17 @@ export class MapsService {
|
||||
source: 'trek-places' as const,
|
||||
})),
|
||||
source: 'trek-places' as const,
|
||||
truncated: found.length >= 50,
|
||||
truncated: found.length >= cap,
|
||||
// A wide viewport is narrowed here too, and the caller is told so
|
||||
// for the same reason the Overpass path tells it.
|
||||
clamped: radius < wanted,
|
||||
clamped: radius < reach,
|
||||
};
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.warn('TREK Places nearby failed, falling back:', (err as Error).message);
|
||||
}
|
||||
}
|
||||
return this.searchOverpassPois(category, bbox, lang);
|
||||
return this.searchOverpassPois(category, bbox, lang, limit);
|
||||
}
|
||||
|
||||
// ── API key retrieval ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -261,6 +261,14 @@ export const POI_CATEGORY_TO_TREK: Record<string, string[]> = {
|
||||
activity: ['amusement_park', 'zoo', 'aquarium', 'water_park', 'theme_park'],
|
||||
shopping: ['shopping', 'shopping_center', 'department_store', 'market'],
|
||||
supermarket: ['grocery_store', 'food_and_beverage_store', 'convenience_store', 'supermarket'],
|
||||
// What a drive needs rather than what a city visit does (#1797). The corridor
|
||||
// search asks for these by name, and without them every corridor query fell
|
||||
// through to Overpass — the one path where that hurts most, because a single
|
||||
// search fans out over sixteen boxes.
|
||||
fuel: ['gas_station', 'fueling_station'],
|
||||
charging: ['ev_charging_station'],
|
||||
rest_area: ['rest_stop'],
|
||||
campsite: ['campground', 'rv_park'],
|
||||
};
|
||||
|
||||
export interface TrekNearbyPlace extends TrekPlace {
|
||||
|
||||
@@ -1167,16 +1167,17 @@ export class PlacesService {
|
||||
}
|
||||
if (!wp.name) continue;
|
||||
try {
|
||||
// Background lane, which is what this method's own contract asks of a
|
||||
// bulk caller: up to thirty sequential lookups, each taking the next
|
||||
// slot on a 1.1 s process-wide throttle. On the interactive lane every
|
||||
// other member of the instance typing in the place search box waits
|
||||
// behind the whole import.
|
||||
const hits = await this.maps.searchNominatim(wp.name, undefined, 'background');
|
||||
const hit = hits.find((h) => h.lat !== null && h.lng !== null);
|
||||
// Through geocodeQuery, which asks the TREK index first and only falls
|
||||
// through to Nominatim for what it does not know — and does so on the
|
||||
// BACKGROUND lane. That matters here more than anywhere: this loop runs
|
||||
// up to thirty times in one request, each Nominatim call taking the next
|
||||
// slot on a 1.1 s process-wide throttle, so on the interactive lane one
|
||||
// pasted link made everybody else's place search queue behind it for
|
||||
// half a minute. An index hit costs no slot at all.
|
||||
const hit = await this.maps.geocodeQuery(wp.name);
|
||||
// The name from the link, not the one the geocoder answers with: somebody who
|
||||
// typed a nickname into Google should not find a street address on their trip.
|
||||
if (hit) places.push({ name: wp.name, lat: hit.lat!, lng: hit.lng!, notes: null, googleFtid: null });
|
||||
if (hit) places.push({ name: wp.name, lat: hit.lat, lng: hit.lng, notes: null, googleFtid: null });
|
||||
else unplaceable++;
|
||||
} catch {
|
||||
// A geocoder that is down or rate-limited costs this one stop, not the import.
|
||||
|
||||
Reference in New Issue
Block a user