import { describe, expect, it } from "vitest"; import { canonicalizeMarinePois, normalizedMarineFacilityName, type MarinePoiCandidate } from "../src/marine-poi-clustering.js"; const lock = ( id: string, name: string | null, lon: number, properties: Record = {}, overrides: Partial = {} ): MarinePoiCandidate => ({ id, layer: "locks", source: "osm", sourceId: id, name, coordinate: { lon, lat: 51.695 }, properties, ...overrides }); describe("marine POI canonicalization", () => { it("normalizes facility type words but retains facility ordinals", () => { expect(normalizedMarineFacilityName("Schleuse Werries")).toBe("werries"); expect(normalizedMarineFacilityName("Schleusengebiet Werries")).toBe("werries"); expect(normalizedMarineFacilityName("Werries Lock")).toBe("werries"); expect(normalizedMarineFacilityName("W.S.V. Sixhaven")).toBe("sixhaven"); expect(normalizedMarineFacilityName("Große Kammer Schleuse Ahsen")).toBe("ahsen"); expect(normalizedMarineFacilityName("Schleuse Ahsen Kammer 2")).toBe("ahsen"); expect(normalizedMarineFacilityName("Schleuse 55")).toBe("55"); expect(normalizedMarineFacilityName("Außentor")).toBeNull(); }); it("merges matching OSM and EuRIS locks and combines their contacts", () => { const result = canonicalizeMarinePois([ lock("w-osm", "Schleuse Werries", 7.867, { website: "https://osm.example/werries", phone: "+49 2381 9019290" }), lock( "w-euris", "Werries", 7.86708, { "ref:EU:RIS": "DEHMM00301LOCKS00404", phone: "+49 2381 9019-290", vhf: "22" }, { source: "euris", sourceId: "DEHMM00301LOCKS00404" } ) ]); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ entityId: "marine-poi:locks:euris:DEHMM00301LOCKS00404", canonicalSource: "euris", memberCount: 2, properties: { phone: "+49 2381 9019-290", website: "https://osm.example/werries", vhf: "22", source: "EuRIS + OpenStreetMap" } }); }); it("does not merge neighbouring facilities with different specific names", () => { const result = canonicalizeMarinePois([ lock("a", "Wilhelminasluis", 4.724), lock("b", "Hondsbossche Sluis", 4.72418) ]); expect(result).toHaveLength(2); }); it("merges duplicate facilities with the exact same name despite differing source websites", () => { const result = canonicalizeMarinePois([ lock("relation", "Kesselschleuse Emden", 7.2, { website: "https://city.example/lock" }), lock("way", "Kesselschleuse Emden", 7.20008, { website: "https://operator.example/lock" }) ]); expect(result).toHaveLength(1); expect(result[0]?.properties.websites).toEqual([ "https://city.example/lock", "https://operator.example/lock" ]); }); it("does not merge equal names carrying conflicting official IDs", () => { const result = canonicalizeMarinePois([ lock("a", "Schleuse 55", 7.1, { "ref:EU:RIS": "DE-55" }), lock("b", "Schleuse 55", 7.1001, { "ref:EU:RIS": "DE-56" }) ]); expect(result).toHaveLength(2); }); it("assigns an unnamed gate to exactly the nearest anchor without bridging facilities", () => { const result = canonicalizeMarinePois([ lock("west", "Schleuse West", 7), lock("east", "Schleuse Ost", 7.0018), lock("gate", "Außentor", 7.0002, { waterway: "lock_gate" }) ]); expect(result).toHaveLength(2); expect(result.find((poi) => poi.name === "Schleuse West")?.memberIds).toContain("gate"); expect(result.find((poi) => poi.name === "Schleuse Ost")?.memberIds).not.toContain("gate"); }); it("groups a named harbour anchor with its dock component", () => { const result = canonicalizeMarinePois([ { id: "marina", layer: "harbours", source: "osm", sourceId: "w1", name: "Stadthafen", coordinate: { lon: 7.7, lat: 52 }, properties: { leisure: "marina" } }, { id: "dock", layer: "harbours", source: "osm", sourceId: "w2", name: "Stadthafen", coordinate: { lon: 7.7004, lat: 52 }, properties: { waterway: "dock" } } ]); expect(result).toHaveLength(1); expect(result[0]?.memberCount).toBe(2); }); it("is independent of input order and adding a gate keeps the anchor ID stable", () => { const anchor = lock("anchor", "Kesselschleuse Emden", 7.2, { lock: "yes" }); const gate = lock("gate", null, 7.2001, { waterway: "lock_gate" }); const forward = canonicalizeMarinePois([anchor, gate]); const reverse = canonicalizeMarinePois([gate, anchor]); const anchorOnly = canonicalizeMarinePois([anchor]); expect(forward).toEqual(reverse); expect(forward[0]?.entityId).toBe(anchorOnly[0]?.entityId); }); it("keeps same-named facilities apart beyond the layer radius", () => { const result = canonicalizeMarinePois([ lock("one", "Keersluis", 5), lock("two", "Keersluis", 5.02) ]); expect(result).toHaveLength(2); }); });