Files
TREK/server/tests/unit/nest/photon.mapping.test.ts
T
Maurice 1e5314d847 maps, routing, i18n: settle what the checks found
- The lead is Photon's own row first; a row of its name inside the lead
  only stands in where Photon's row went as the same place. Rows without a
  line below the name are never folded as reading the same.
- A miss of Photon where it is the geocoder no longer asks it a second time
  on the way to an empty answer.
- The transport words of the identity match use a Unicode word boundary
  (straße, estación) and match 駅 anywhere.
- Opening an OSM place reads the coordinate it was placed at explicitly.
- The ferry spans of a Valhalla leg move to valhallaFerries.ts, and a day
  cut into pieces is asked with one options object.
- "Limit" on the allowance tiles is "Daily limit", which no locale left
  as an English copy.
- Tests follow the new behaviour: no Nominatim behind Photon, zoom 13,
  and the expectations of the merge and edit-distance cases.
2026-10-30 16:36:22 +01:00

355 lines
14 KiB
TypeScript

/**
* PHOTON-MAP-001..029: Photon's answers read into the shape Nominatim gives,
* the rows of the suggestion list, the lead the list opens on, and the
* language Photon is asked in.
*/
import {
asLead,
editDistance,
nameLike,
pickLead,
rankFeatures,
asNominatimHit,
asNominatimReverse,
asSuggestion,
photonAnswerSchema,
photonDisplayName,
photonLang,
type PhotonFeature,
} from '../../../src/nest/maps/providers/photon.mapping';
import { gateFeature } from '../../helpers/photon';
import { describe, expect, it } from 'vitest';
const feature = (properties: Record<string, unknown> = {}): PhotonFeature =>
photonAnswerSchema.parse({ features: [gateFeature(properties)] }).features[0];
describe('photonLang', () => {
it('PHOTON-MAP-001: asks in the languages the import holds names for', () => {
expect(photonLang('de')).toBe('de');
expect(photonLang('en')).toBe('en');
expect(photonLang('ja')).toBe('ja');
});
it("PHOTON-MAP-002: reads TREK's own codes through toApiLang first", () => {
expect(photonLang('br')).toBe('pt');
expect(photonLang('gr')).toBe('el');
});
it('PHOTON-MAP-003: drops the region, whatever its case', () => {
expect(photonLang('pt-BR')).toBe('pt');
expect(photonLang('zh-TW')).toBe('zh');
expect(photonLang('EN-gb')).toBe('en');
});
it('PHOTON-MAP-004: asks for the names on the spot in a language without names, and without one', () => {
expect(photonLang('eo')).toBe('default');
expect(photonLang('xx-YY')).toBe('default');
expect(photonLang(undefined)).toBe('default');
expect(photonLang(' ')).toBe('default');
});
});
describe('photonDisplayName', () => {
it('PHOTON-MAP-005: names the place, then house number and street as Nominatim does, quarter, town, region, postcode and country', () => {
expect(photonDisplayName(feature({ housenumber: '1' }).properties)).toBe(
'Brandenburger Tor, 1, Pariser Platz, Mitte, Berlin, 10117, Deutschland',
);
});
it('PHOTON-MAP-013: writes an address without a name the way Nominatim writes a house', () => {
const house = feature({ name: undefined, housenumber: '77', street: 'Unter den Linden', postcode: undefined });
expect(photonDisplayName(house.properties)).toBe('77, Unter den Linden, Mitte, Berlin, Deutschland');
});
it('PHOTON-MAP-006: leaves out what is missing and what repeats', () => {
const city = feature({ name: 'Berlin', street: undefined, district: undefined, postcode: undefined, type: 'city' });
expect(photonDisplayName(city.properties)).toBe('Berlin, Deutschland');
});
});
describe('asNominatimHit', () => {
it('PHOTON-MAP-007: spells the hit the way a Nominatim search row is spelt', () => {
expect(asNominatimHit(feature())).toEqual({
osm_type: 'way',
osm_id: '518071791',
class: 'tourism',
type: 'attraction',
name: 'Brandenburger Tor',
display_name: 'Brandenburger Tor, Pariser Platz, Mitte, Berlin, 10117, Deutschland',
lat: '52.5163',
lon: '13.3777',
extratags: { wikidata: 'Q82425', wikipedia: 'de:Brandenburger Tor' },
});
});
it('PHOTON-MAP-008: names nodes and relations in full, and keeps only text tags', () => {
expect(asNominatimHit(feature({ osm_type: 'N' }))?.osm_type).toBe('node');
expect(asNominatimHit(feature({ osm_type: 'R' }))?.osm_type).toBe('relation');
expect(asNominatimHit(feature({ extra: { wikidata: 'Q1', capacity: 12 } }))?.extratags).toEqual({ wikidata: 'Q1' });
expect(asNominatimHit(feature({ extra: undefined }))?.extratags).toBeNull();
});
it('PHOTON-MAP-009: has nothing for a hit without an OpenStreetMap id', () => {
expect(asNominatimHit(feature({ osm_type: undefined }))).toBeNull();
expect(asNominatimHit(feature({ osm_type: 'X' }))).toBeNull();
expect(asNominatimHit(feature({ osm_id: undefined }))).toBeNull();
});
});
describe('asNominatimReverse', () => {
it('PHOTON-MAP-010: fills the address fields Nominatim fills, the sight under its main tag', () => {
expect(asNominatimReverse(feature({ housenumber: '1' }))).toEqual({
display_name: 'Brandenburger Tor, 1, Pariser Platz, Mitte, Berlin, 10117, Deutschland',
name: 'Brandenburger Tor',
address: {
road: 'Pariser Platz',
house_number: '1',
postcode: '10117',
suburb: 'Mitte',
city: 'Berlin',
state: 'Berlin',
country: 'Deutschland',
country_code: 'de',
tourism: 'Brandenburger Tor',
},
});
});
it('PHOTON-MAP-011: makes a town its own city and files nothing more under its tag', () => {
const town = feature({ name: 'Warnemünde', type: 'city', osm_key: 'place', city: undefined, street: undefined });
const reverse = asNominatimReverse(town);
expect(reverse.address?.city).toBe('Warnemünde');
expect(reverse.address).not.toHaveProperty('place');
});
it('PHOTON-MAP-012: a feature with nothing to name has no display name', () => {
const bare = photonAnswerSchema.parse({ features: [{ geometry: { coordinates: [0, 0] }, properties: {} }] });
expect(asNominatimReverse(bare.features[0])).toEqual({ display_name: undefined, name: undefined, address: {} });
});
});
describe('asSuggestion', () => {
it('PHOTON-MAP-014: titles the row by its name, with where it is below, in the id form the details lookup reads', () => {
expect(asSuggestion(feature())).toEqual({
placeId: 'way:518071791',
mainText: 'Brandenburger Tor',
secondaryText: 'Pariser Platz, Mitte, Berlin, Deutschland',
source: 'openstreetmap',
lat: 52.5163,
lng: 13.3777,
});
});
it('PHOTON-MAP-015: keeps a name that holds a comma whole, and titles a house without one by its number', () => {
const bar = asSuggestion(feature({ name: 'Bar, Café Einstein' }));
expect(bar?.mainText).toBe('Bar, Café Einstein');
expect(bar?.secondaryText).toBe('Pariser Platz, Mitte, Berlin, Deutschland');
const house = asSuggestion(
feature({ name: undefined, housenumber: '77', street: 'Unter den Linden', postcode: undefined }),
);
expect(house?.mainText).toBe('77');
expect(house?.secondaryText).toBe('Unter den Linden, Mitte, Berlin, Deutschland');
});
it('PHOTON-MAP-016: has no row for a hit without an id or anything to call it', () => {
expect(asSuggestion(feature({ osm_id: undefined }))).toBeNull();
const bare = photonAnswerSchema.parse({
features: [{ geometry: { coordinates: [13.4, 52.5] }, properties: { osm_type: 'N', osm_id: 1 } }],
});
expect(asSuggestion(bare.features[0])).toBeNull();
});
});
describe('asSuggestion, the line below the name', () => {
it('PHOTON-MAP-017: leaves out postcode and county, and names the region only where there is no town', () => {
const village = asSuggestion(
feature({
osm_key: 'place',
osm_value: 'village',
name: 'Berlin',
street: undefined,
postcode: '457130',
district: undefined,
city: undefined,
county: 'Троицкий муниципальный округ',
state: 'Chelyabinsk Oblast',
country: 'Russia',
}),
);
expect(village?.secondaryText).toBe('Chelyabinsk Oblast, Russia');
expect(asSuggestion(feature({ county: 'Berlin', postcode: '10117' }))?.secondaryText).toBe(
'Pariser Platz, Mitte, Berlin, Deutschland',
);
});
});
describe('asLead', () => {
const city = (properties: Record<string, unknown> = {}) =>
feature({
osm_type: 'R',
osm_id: 62422,
osm_key: 'place',
osm_value: 'city',
type: 'city',
name: 'Berlin',
street: undefined,
postcode: undefined,
district: undefined,
city: undefined,
extent: [13.088345, 52.6755087, 13.7611609, 52.3382448],
...properties,
});
it('PHOTON-MAP-018: reads a town as a lead from nearby, with its extent', () => {
expect(asLead(city())).toEqual({
placeId: 'relation:62422',
name: 'Berlin',
reach: 'nearby',
kind: 'city',
lat: 52.5163,
lng: 13.3777,
extent: [13.088345, 52.6755087, 13.7611609, 52.3382448],
});
});
it('PHOTON-MAP-019: reads a country or a region as a lead from everywhere, extent or none', () => {
expect(asLead(city({ osm_value: 'country', type: 'country', name: 'Mauritania' }))?.reach).toBe('everywhere');
// Tokyo is a province Photon files on its town layer.
expect(asLead(city({ osm_value: 'province', name: 'Tokyo' }))?.reach).toBe('everywhere');
expect(asLead(city({ extent: undefined }))?.extent).toBeNull();
});
it('PHOTON-MAP-020: has no lead for a sight, a street or a quarter', () => {
expect(asLead(feature())).toBeNull();
expect(asLead(city({ osm_key: 'highway', osm_value: 'primary', type: 'street' }))).toBeNull();
expect(asLead(city({ osm_value: 'suburb', type: 'district' }))).toBeNull();
});
});
describe('rankFeatures', () => {
it('PHOTON-MAP-021: puts a place with a Wikidata link before its unlinked namesakes, Photon order otherwise', () => {
const art = feature({
osm_id: 1,
osm_key: 'tourism',
osm_value: 'artwork',
extra: undefined,
name: 'Neuschwanstein',
});
const castle = feature({ osm_id: 2, osm_key: 'tourism', osm_value: 'attraction', name: 'Schloss Neuschwanstein' });
const street = feature({
osm_id: 3,
osm_key: 'highway',
osm_value: 'residential',
extra: undefined,
name: 'Neuschwansteinstraße',
});
const shop = feature({
osm_id: 4,
osm_key: 'shop',
osm_value: 'gift',
extra: undefined,
name: 'Neuschwanstein Souvenirs',
});
expect(rankFeatures([art, street, castle, shop]).map((f) => f.properties.osm_id)).toEqual([2, 1, 3, 4]);
expect(rankFeatures([castle, art]).map((f) => f.properties.osm_id)).toEqual([2, 1]);
});
});
describe('pickLead', () => {
const town = (properties: Record<string, unknown>) =>
feature({
osm_key: 'place',
osm_value: 'city',
type: 'city',
street: undefined,
district: undefined,
city: undefined,
...properties,
});
it('PHOTON-MAP-022: a country or region first leads under any name, so an exonym keeps it', () => {
const lead = pickLead(
[town({ osm_type: 'R', osm_id: 192763, osm_value: 'country', type: 'country', name: 'Mauritania' })],
'Mauretanien',
);
expect(lead).toMatchObject({ placeId: 'relation:192763', reach: 'everywhere' });
});
it('PHOTON-MAP-023: anything else leads only when called what was typed, or a typo of it', () => {
const centrum = town({
osm_type: 'R',
osm_id: 1,
osm_key: 'boundary',
osm_value: 'administrative',
type: 'county',
name: 'Centrum',
});
const paramaribo = town({ osm_type: 'N', osm_id: 2, name: 'Paramaribo' });
expect(pickLead([centrum, paramaribo], 'Paramraibo')).toMatchObject({ placeId: 'node:2' });
expect(pickLead([centrum], 'Paramraibo')).toBeNull();
});
it('PHOTON-MAP-024: no lead for an airport code, nor for a municipality with nothing to say where the traveller is', () => {
expect(pickLead([town({ name: 'FRA' })], 'FRA')).toBeNull();
const municipality = town({ osm_value: 'municipality', name: 'Sagrada Família' });
expect(pickLead([municipality], 'Sagrada Familia')).toBeNull();
expect(pickLead([municipality], 'Sagrada Familia', { lat: 52.5, lng: 13.4 })).not.toBeNull();
});
});
describe('pickLead, namesakes', () => {
const place = (properties: Record<string, unknown>) =>
feature({ osm_key: 'place', street: undefined, district: undefined, city: undefined, ...properties });
it('PHOTON-MAP-027: a namesake ranked higher that may not lead leaves the list without a lead', () => {
const london = place({ osm_type: 'R', osm_id: 1, osm_value: 'town', type: 'district', name: 'Greenwich' });
const connecticut = place({ osm_type: 'R', osm_id: 2, osm_value: 'town', type: 'city', name: 'Greenwich' });
expect(pickLead([london, connecticut], 'Greenwich')).toBeNull();
expect(pickLead([connecticut, london], 'Greenwich')).toMatchObject({ placeId: 'relation:2' });
});
it('PHOTON-MAP-029: a street of the name ranked higher does not block, and an island leads', () => {
const street = place({
osm_type: 'W',
osm_id: 4,
osm_key: 'highway',
osm_value: 'primary',
type: 'street',
name: 'Spitzbergen',
});
const island = place({ osm_type: 'R', osm_id: 5, osm_value: 'island', type: 'other', name: 'Spitzbergen' });
expect(pickLead([street, island], 'Spitzbergen')).toMatchObject({ placeId: 'relation:5', kind: 'island' });
});
it('PHOTON-MAP-028: a town filed as an administrative boundary leads as a town', () => {
const merida = place({
osm_type: 'R',
osm_id: 3,
osm_key: 'boundary',
osm_value: 'administrative',
type: 'city',
name: 'Mérida',
});
expect(pickLead([merida], 'Mérida')).toMatchObject({ placeId: 'relation:3', kind: 'city' });
});
});
describe('nameLike and editDistance', () => {
it('PHOTON-MAP-025: counts a swap of neighbours as one edit', () => {
expect(editDistance('paramraibo', 'paramaribo')).toBe(1);
expect(editDistance('tokio', 'tokyo')).toBe(1);
expect(editDistance('ab', 'ba')).toBe(1);
expect(editDistance('berlin', 'berlin')).toBe(0);
});
it('PHOTON-MAP-026: takes the same name, its start, a word of it or a near typo, and nothing shorter than three letters', () => {
expect(nameLike('Berl', 'Berlin')).toBe(true);
expect(nameLike('Tor', 'Brandenburger Tor')).toBe(true);
expect(nameLike('Tokio', 'Tokyo')).toBe(true);
expect(nameLike('Mailand', 'Milan')).toBe(false);
expect(nameLike('Be', 'Berlin')).toBe(false);
});
});