mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
84b51a8af5
Two consumers decide whether a candidate place is one the trip already has, and they spelled the rule out separately: the bulk importers ask an in-memory set (isPlaceDuplicate), while findDuplicatePlace asks the database because its caller needs the matched row's id. The two had drifted — the SQL copy falls back to a coordinate match for a NAMED candidate, which the in-memory copy deliberately never does, since that would merge the restaurant and the bar at the same address. The order now lives once in @trek/shared as placeMatchStrategies(), and each consumer is a mechanical interpreter of it: provider id (the only part that survives a rename, #1550), then name, then coordinates and only when there is no name. Adds PlacesService.findMatchingPlaceId as the public door for an importer that has to link to the existing place rather than merely skip the candidate. findDuplicatePlace stays private behind it — it also returns google_ftid for the bulk importer's backfill, which is that caller's business and not part of "which place is this?". No behaviour change for either existing caller. The coordinate branch is currently unreachable from the one call site, which sits inside an isPlaceDuplicate() guard that has already matched on name and returned; the drift only bites once something calls it standalone. COORD_DEDUP_TOLERANCE and externalIdsOf move to shared and are re-exported from places.helpers, so collections.service and places.service keep their imports.
75 lines
2.9 KiB
TypeScript
75 lines
2.9 KiB
TypeScript
import { COORD_DEDUP_TOLERANCE, externalIdsOf, normalizePlaceName, placeMatchStrategies } from './place-match';
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
describe('normalizePlaceName', () => {
|
|
it('trims and lowercases', () => {
|
|
expect(normalizePlaceName(' Eiffel Tower ')).toBe('eiffel tower');
|
|
});
|
|
|
|
it('treats blank, whitespace, null and undefined alike as no name', () => {
|
|
expect(normalizePlaceName('')).toBeNull();
|
|
expect(normalizePlaceName(' ')).toBeNull();
|
|
expect(normalizePlaceName(null)).toBeNull();
|
|
expect(normalizePlaceName(undefined)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('externalIdsOf', () => {
|
|
it('collects the provider ids that carry a value, trimmed', () => {
|
|
expect(externalIdsOf({ google_place_id: ' ChIJ_a ', google_ftid: '0x1:0x2', osm_id: null })).toEqual([
|
|
'ChIJ_a',
|
|
'0x1:0x2',
|
|
]);
|
|
});
|
|
|
|
it('ignores blank ids', () => {
|
|
expect(externalIdsOf({ google_ftid: ' ', osm_id: '' })).toEqual([]);
|
|
});
|
|
|
|
it('keeps provider order: place_id, ftid, osm', () => {
|
|
expect(externalIdsOf({ osm_id: 'node/42', google_ftid: 'f', google_place_id: 'p' })).toEqual(['p', 'f', 'node/42']);
|
|
});
|
|
});
|
|
|
|
describe('placeMatchStrategies', () => {
|
|
it('puts every provider id first, one strategy each, in provider order', () => {
|
|
expect(placeMatchStrategies({ name: 'Trattoria', google_place_id: 'p', osm_id: 'node/42' })).toEqual([
|
|
{ by: 'externalId', id: 'p' },
|
|
{ by: 'externalId', id: 'node/42' },
|
|
{ by: 'name', name: 'trattoria' },
|
|
]);
|
|
});
|
|
|
|
it('never offers coordinates for a NAMED candidate, even when it has them', () => {
|
|
// The whole point of the rule: widening the coordinate check to named places
|
|
// would merge the restaurant and the bar in the same building.
|
|
const strategies = placeMatchStrategies({ name: 'Ground Floor Diner', lat: 52.52, lng: 13.405 });
|
|
|
|
expect(strategies).toEqual([{ by: 'name', name: 'ground floor diner' }]);
|
|
expect(strategies.some((s) => s.by === 'coords')).toBe(false);
|
|
});
|
|
|
|
it('falls back to coordinates only when there is no name', () => {
|
|
expect(placeMatchStrategies({ name: null, lat: 48.85, lng: 2.35 })).toEqual([
|
|
{ by: 'coords', lat: 48.85, lng: 2.35, tolerance: COORD_DEDUP_TOLERANCE },
|
|
]);
|
|
});
|
|
|
|
it('offers provider ids then coordinates for an unnamed candidate that has both', () => {
|
|
expect(placeMatchStrategies({ name: ' ', google_ftid: 'f', lat: 1, lng: 2 })).toEqual([
|
|
{ by: 'externalId', id: 'f' },
|
|
{ by: 'coords', lat: 1, lng: 2, tolerance: COORD_DEDUP_TOLERANCE },
|
|
]);
|
|
});
|
|
|
|
it('offers nothing for a candidate with no name, no ids and no coordinates', () => {
|
|
expect(placeMatchStrategies({ name: null, lat: null, lng: null })).toEqual([]);
|
|
});
|
|
|
|
it('needs both halves of a coordinate pair', () => {
|
|
expect(placeMatchStrategies({ name: null, lat: 48.85, lng: null })).toEqual([]);
|
|
expect(placeMatchStrategies({ name: null, lat: null, lng: 2.35 })).toEqual([]);
|
|
});
|
|
});
|