// A place, between the geocoder and the two forms that record one. Both the // dive site form and a trip part's row fill the same object from the same // response, so the mapping is written once here rather than slightly // differently in each of them. import type { GeocodeResult } from "@/lib/api/geocoding"; import type { LocationFormValue } from "@/lib/validations/location"; /** * A geocoder result as a place. * * `name` is the API's `location` as it arrives - the place, its region and its * country, "Ko Tao, Surat Thani Province, Thailand" - for the site dialog and * the trip form alike. The API composes it once, for a typed search and a * dropped pin both, so nothing here rebuilds it from parts. * * The centre and the box are the *place's*, which is what a forward search * answers with. A reverse geocode must not go through here: the coordinates it * returns are the host's own position, so adopting them would file the pin a * diver dropped as the centre of the town around it. */ export function geocodeResultToLocation( result: GeocodeResult, ): LocationFormValue { return { name: result.location, latitude: result.latitude, longitude: result.longitude, bbox_south: result.bbox_south, bbox_north: result.bbox_north, bbox_west: result.bbox_west, bbox_east: result.bbox_east, }; } /** * Where a place sits, as one string: `region, country` where both are known, * whichever one is where only one is, and `null` where neither is. * * What the dive site search shows for a geocoder row. */ export function formatPlaceContext( region?: string | null, country?: string | null, ): string | null { const parts = [region, country].filter( (part): part is string => !!part?.trim(), ); return parts.length > 0 ? parts.join(", ") : null; } /** * A geocoder result as the dive site search shows it: the place's own name, and * where it is as `region, country` - "Moalboal" and "Cebu, Philippines", read * together as "Moalboal, Cebu, Philippines". * * Read as one line, the row says what picking it saves: the API's `location` is * the same three parts. A result that matched an address has no name of its * own, so it reads as its `location` alone, which already holds every part a * hint would add. A context part the name already says is dropped - the country * row "Philippines" gets no context at all. */ export function geocodeResultLabel(result: GeocodeResult): { name: string; context: string | null; } { const name = result.name; if (!name) return { name: result.location, context: null }; const region = unrepeated(result.region, name); const country = unrepeated(result.country, name, region); return { name, context: formatPlaceContext(region, country) }; } /** * `part`, unless it only repeats a comma-separated part of one of `labels` - * compared whole and case-insensitively, so "Cebu" repeats "Moalboal, Cebu" but * not "Cebu City". `null` for a repeat and for a blank. * * A menu row joins its name and hint with ", ", so a hint part the name already * holds reads twice: the country row "Philippines" would otherwise say * "Philippines, Philippines". */ export function unrepeated( part: string | null | undefined, ...labels: (string | null | undefined)[] ): string | null { const trimmed = part?.trim(); if (!trimmed) return null; const said = labels.flatMap((label) => (label ?? "").split(",").map((each) => each.trim().toLowerCase()), ); return said.includes(trimmed.toLowerCase()) ? null : trimmed; }