// search.js — search box logic (pure): coordinates, H3 cell ids and pin matches → suggestions import { filterPins } from './pins.js'; import { fmtLatLon } from './units.js'; const HEMI = { N: ['lat', 1], S: ['lat', -1], E: ['lon', 1], W: ['lon', -1] }; const MARKS = ['°', "'", '"']; function tokens(q) { return q.toUpperCase() .replace(/[°º]/g, ' ° ').replace(/['′’]/g, " ' ").replace(/["″”]/g, ' " ') .replace(/([NSEW])/g, ' $1 ').replace(/[,;]/g, ' , ') .split(/\s+/).filter(Boolean); } export function parseCoords(q) { // "lat,lon" first; failing that, commas between digits are decimal commas return parseCoordsAs(q) ?? (/\d,\d/.test(q) ? parseCoordsAs(q.replace(/(\d),(\d)/g, '$1.$2')) : null); } function parseCoordsAs(q) { const comps = []; let cur = null; const close = () => { if (cur?.parts.length) comps.push(cur); cur = null; }; const toks = tokens(q); for (let i = 0; i < toks.length; i++) { const t = toks[i]; if (t === ',') { close(); continue; } if (HEMI[t]) { if (cur?.parts.length && !cur.hemi) { cur.hemi = t; close(); } else { close(); cur = { parts: [], units: [], hemi: t }; } continue; } if (!/^[+-]?\d+(\.\d+)?$/.test(t)) return null; const mark = MARKS.includes(toks[i + 1]) ? MARKS.indexOf(toks[++i]) : null; if (cur?.parts.length && (mark === null || mark <= cur.units.at(-1))) close(); // bare number or restarting unit cur ??= { parts: [], units: [], hemi: null }; cur.parts.push(parseFloat(t)); cur.units.push(mark ?? 0); } close(); if (comps.length !== 2 || comps.some(c => c.parts.slice(1).some(v => v < 0 || v >= 60))) return null; const val = c => c.parts.reduce((a, v, k) => a + Math.abs(v) / 60 ** c.units[k], 0) * (c.parts[0] < 0 ? -1 : 1); const out = {}; const lettered = comps.filter(c => c.hemi); for (const c of lettered) { const [axis, sign] = HEMI[c.hemi]; if (axis in out) return null; out[axis] = Math.abs(val(c)) * sign; } const bare = comps.filter(c => !c.hemi); for (const c of bare) out['lat' in out ? 'lon' : 'lat'] = val(c); if (!(Math.abs(out.lat) <= 90) || !(Math.abs(out.lon) <= 180)) return null; return { lat: out.lat, lon: out.lon }; } export const isCellId = q => /^8[0-9a-f]{14}$/i.test(q.trim()); export function suggest(q, pins, max = 8) { const s = q.trim(); if (!s) return []; const out = []; const c = parseCoords(s); if (c) out.push({ kind: 'coords', label: fmtLatLon(c.lat, c.lon), ...c }); if (isCellId(s)) out.push({ kind: 'cell', label: `H3 cell ${s.toLowerCase()}`, cell: s.toLowerCase() }); for (const p of filterPins(pins, s).slice(0, Math.max(0, max - out.length))) { out.push({ kind: 'pin', label: p.name, sub: p.lore || fmtLatLon(p.lat, p.lon), lat: p.lat, lon: p.lon }); } return out; }