mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
Match a place name across its inflected forms, and forget an empty answer quickly
This commit is contained in:
@@ -95,13 +95,29 @@ export function namesOverlap(a: string, b: string): boolean {
|
||||
.split(/[^a-z0-9]+/)
|
||||
.filter((word) => word.length > 2),
|
||||
);
|
||||
const left = words(a);
|
||||
if (left.size === 0) return false;
|
||||
const left = [...words(a)];
|
||||
if (left.length === 0) return false;
|
||||
|
||||
/**
|
||||
* Same word, allowing for an inflected ending.
|
||||
*
|
||||
* Providers localise names, and the two sides of this comparison rarely come
|
||||
* from the same language: searching "Hamburg Airport" returns "Hamburger
|
||||
* Flughafen Helmut Schmidt", and a plain equality test rejects its own
|
||||
* correct answer over the "-er". A short prefix would over-match, so the
|
||||
* shared stem has to carry weight and the endings have to be close.
|
||||
*/
|
||||
const sameStem = (x: string, y: string): boolean => {
|
||||
if (x === y) return true;
|
||||
const [short, long] = x.length <= y.length ? [x, y] : [y, x];
|
||||
return short.length >= 4 && long.length - short.length <= 3 && long.startsWith(short);
|
||||
};
|
||||
|
||||
let shared = 0;
|
||||
for (const word of words(b)) {
|
||||
if (!left.has(word)) continue;
|
||||
if (word.length >= 4) return true;
|
||||
const match = left.find((candidate) => sameStem(candidate, word));
|
||||
if (!match) continue;
|
||||
if (Math.min(match.length, word.length) >= 4) return true;
|
||||
if (++shared >= 2) return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -34,6 +34,21 @@ const GOOGLE_CAP = 3;
|
||||
*/
|
||||
const CACHE_KIND = 2;
|
||||
const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
/**
|
||||
* A week is right for an answer, and far too long for the absence of one.
|
||||
*
|
||||
* Nothing found is usually the provider having a bad minute — a Wikimedia
|
||||
* timeout, a rate limit, a Nominatim hiccup — not a place that genuinely has no
|
||||
* pictures and no article. Storing that under the same TTL means one unlucky
|
||||
* request leaves the column blank for the rest of the week, which is exactly
|
||||
* how a fix can look like no fix at all when it lands.
|
||||
*/
|
||||
const EMPTY_CACHE_TTL_MS = 10 * 60 * 1000;
|
||||
|
||||
/** Whether an answer is worth keeping for a week. */
|
||||
function hasAnything(value: CachedEnrichment): boolean {
|
||||
return !!(value.photos.length || value.description || value.facts.length || value.hours || value.rating);
|
||||
}
|
||||
/**
|
||||
* Bumped whenever the shape or the sources change. Without it a release that
|
||||
* adds a field or swaps a provider keeps serving the previous week's answers
|
||||
@@ -593,7 +608,7 @@ export class PlaceEnrichmentService {
|
||||
lang ?? '',
|
||||
CACHE_KIND,
|
||||
);
|
||||
if (!row || Date.now() - row.fetched_at >= CACHE_TTL_MS) return null;
|
||||
if (!row) return null;
|
||||
const parsed = JSON.parse(row.payload_json) as CachePayload;
|
||||
if (parsed.v !== CACHE_VERSION) return null;
|
||||
|
||||
@@ -604,13 +619,18 @@ export class PlaceEnrichmentService {
|
||||
// of the week for a place that has pictures perfectly available.
|
||||
const photos = parsed.photos.filter((photo) => this.photoCache.get(photo.key));
|
||||
if (photos.length !== parsed.photos.length) return null;
|
||||
return {
|
||||
|
||||
const value: CachedEnrichment = {
|
||||
photos,
|
||||
description: parsed.description ?? null,
|
||||
facts: parsed.facts ?? [],
|
||||
hours: parsed.hours ?? null,
|
||||
rating: parsed.rating ?? null,
|
||||
};
|
||||
// An answer keeps for a week; the absence of one gets ten minutes.
|
||||
const ttl = hasAnything(value) ? CACHE_TTL_MS : EMPTY_CACHE_TTL_MS;
|
||||
if (Date.now() - row.fetched_at >= ttl) return null;
|
||||
return value;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -81,6 +81,18 @@ describe('namesOverlap', () => {
|
||||
expect(namesOverlap('Café Zürich', 'Cafe Zurich')).toBe(true);
|
||||
});
|
||||
|
||||
it('MAPS-162c: matches a name across an inflected ending', () => {
|
||||
// Live case: searching "Hamburg Airport" returns OSM's "Hamburger
|
||||
// Flughafen Helmut Schmidt". Insisting on exact words rejected the correct
|
||||
// answer over an "-er", and the airport came back with nothing at all.
|
||||
expect(namesOverlap('Hamburg Airport', 'Hamburger Flughafen Helmut Schmidt')).toBe(true);
|
||||
expect(namesOverlap('Berlin Hauptbahnhof', 'Berliner Hauptbahnhof')).toBe(true);
|
||||
// Still not a licence to match on a shared opening: the stem has to carry
|
||||
// weight and the endings have to be close.
|
||||
expect(namesOverlap('Bahn Museum', 'Bahnhofsvorplatz Kiosk')).toBe(false);
|
||||
expect(namesOverlap('Alt Museum', 'Altenpflegeheim Nord')).toBe(false);
|
||||
});
|
||||
|
||||
it('MAPS-162b: will not match on a single short word', () => {
|
||||
// An article, and TREK speaks 23 languages — a stopword list for all of
|
||||
// them is its own problem, so weight decides instead.
|
||||
|
||||
Reference in New Issue
Block a user