2026-07-11 00:29:47 +00:00
|
|
|
"use strict";
|
|
|
|
|
// Shared modal "location picker". Every view's Find button opens this overlay:
|
|
|
|
|
// a search box + a Leaflet map. Searching a place jumps the map (and a matching
|
|
|
|
|
// result selects it immediately); tapping anywhere on the map drops a pin the user
|
|
|
|
|
// can fine-tune before confirming. Picking closes the overlay and hands the chosen
|
|
|
|
|
// lat/lon back to the page via the onPick callback.
|
|
|
|
|
//
|
|
|
|
|
// Loaded on every page AFTER leaflet.js and BEFORE that page's own script. The map
|
|
|
|
|
// itself is created lazily the first time the picker opens (and reused after), so
|
|
|
|
|
// pages that never open it pay nothing.
|
|
|
|
|
(function () {
|
2026-07-11 08:10:15 +00:00
|
|
|
let overlay, mapEl, map, marker, pin = null, onPickCb = null, pinIcon = null;
|
2026-07-11 00:29:47 +00:00
|
|
|
let searchForm, searchInput, sugEl, confirmBtn, hintEl;
|
Typo-tolerant location search suggestions (#33)
Add /api/v2/suggest and wire the location picker's search box to it as a
debounced type-ahead: top-5 place suggestions that tolerate a single-letter
typo (substituted, missing, or extra letter, or two adjacent letters swapped)
anywhere in the query, including the first character.
- backend/places.py: local place index built from a GeoNames cities dump
(downloaded once into data/geonames/, loaded in a background thread; the
app boots and serves without it). Exact-prefix matches rank first by
population, then names one edit away; a token vocabulary respells one
mistyped word against known place-name tokens ("pest seattle" ->
"west seattle") for retry against the upstream geocoder, which covers
neighbourhood-level places the dump lacks. THERMOGRAPH_CITIES picks the
dump (default cities1000).
- /suggest blends local and upstream results by population with an exactness
boost, so "Seatle" (a Cumbrian hamlet) can't outrank Seattle when the
query is one edit from the city, while "munchen" still surfaces Munich via
upstream (the index only knows English names). Upstream lookups are
memoized and skipped entirely when the index answers convincingly.
- mappicker.js: debounced (250ms) live suggestions with abort + sequence
guards against stale responses, arrow-key navigation, Enter-picks-highlight,
Escape dismissing the list before closing the overlay. Submit goes through
the same typo-tolerant endpoint.
- climate.geocode results now carry population (used for ranking).
2026-07-11 15:36:14 +00:00
|
|
|
let sugSeq = 0, sugTimer = 0, sugAbort = null;
|
2026-07-11 00:29:47 +00:00
|
|
|
|
|
|
|
|
const PIN_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>`;
|
|
|
|
|
|
|
|
|
|
function build() {
|
|
|
|
|
overlay = document.createElement("div");
|
|
|
|
|
overlay.className = "mp-overlay";
|
|
|
|
|
overlay.hidden = true;
|
|
|
|
|
overlay.innerHTML = `
|
|
|
|
|
<div class="mp-modal" role="dialog" aria-modal="true" aria-label="Pick a location">
|
|
|
|
|
<div class="mp-head">
|
|
|
|
|
<h2>Find a location</h2>
|
|
|
|
|
<button type="button" class="mp-close" aria-label="Close">×</button>
|
|
|
|
|
</div>
|
|
|
|
|
<form class="mp-search" autocomplete="off">
|
Worldwide coverage: grade any point on Earth (#32)
Remove the US+Canada bounding box so every endpoint accepts any lat/lon.
The grading pipeline was already global-ready (ERA5 archive, timezone=auto,
day-of-year climatology), so opening it up is mostly deleting the guard —
plus the edge cases that only exist once the whole globe is in play:
- grid.py: snap() wraps longitude into [-180, 180) and clamps latitude, and
cell centers are normalized so the polar row and the cells straddling the
antimeridian always report valid coordinates to the weather/geocoding APIs.
snap() and from_id() now share one _cell() builder, making id round-trips
exact by construction (verified with a 300k-point global sweep).
- nav.js: neighbor-cell prefetch skips rows past the poles and wraps
longitudes across the dateline instead of sending out-of-range queries.
- Nominatim reverse geocoding requests accept-language=en so place labels
render in one script worldwide (matching the forward geocoder).
- mappicker: search suggestions are no longer filtered to US/CA, the
placeholder and default map view are worldwide.
- calendar: season filter labels flip for southern-hemisphere locations
(Dec-Feb shows as Summer); the underlying month groups are unchanged, so
saved filter selections keep meaning the same months.
Verified end-to-end on a scratch server: Tokyo and Sydney grade with real
labels, a Fiji cell on the antimeridian's east edge builds and serves warm
hits from the derived store, and prefetch=1 on a cold cell still answers
204 without spending weather-API quota.
2026-07-11 15:02:28 +00:00
|
|
|
<input type="text" placeholder="Search for a place…" autocomplete="off" />
|
2026-07-11 00:29:47 +00:00
|
|
|
<button type="submit">Search</button>
|
|
|
|
|
<ul class="mp-sug" hidden></ul>
|
|
|
|
|
</form>
|
|
|
|
|
<div class="mp-map"></div>
|
|
|
|
|
<div class="mp-foot">
|
|
|
|
|
<span class="mp-hint">Search above, or tap the map to drop a pin.</span>
|
|
|
|
|
<button type="button" class="mp-confirm" disabled>Use this location</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>`;
|
|
|
|
|
document.body.appendChild(overlay);
|
|
|
|
|
|
|
|
|
|
mapEl = overlay.querySelector(".mp-map");
|
|
|
|
|
searchForm = overlay.querySelector(".mp-search");
|
|
|
|
|
searchInput = searchForm.querySelector("input");
|
|
|
|
|
sugEl = overlay.querySelector(".mp-sug");
|
|
|
|
|
confirmBtn = overlay.querySelector(".mp-confirm");
|
|
|
|
|
hintEl = overlay.querySelector(".mp-hint");
|
|
|
|
|
|
|
|
|
|
overlay.querySelector(".mp-close").onclick = close;
|
|
|
|
|
// Tapping the dimmed backdrop (outside the modal) closes the picker.
|
|
|
|
|
overlay.addEventListener("pointerdown", (e) => { if (e.target === overlay) close(); });
|
|
|
|
|
confirmBtn.onclick = () => finish(pin);
|
|
|
|
|
|
Typo-tolerant location search suggestions (#33)
Add /api/v2/suggest and wire the location picker's search box to it as a
debounced type-ahead: top-5 place suggestions that tolerate a single-letter
typo (substituted, missing, or extra letter, or two adjacent letters swapped)
anywhere in the query, including the first character.
- backend/places.py: local place index built from a GeoNames cities dump
(downloaded once into data/geonames/, loaded in a background thread; the
app boots and serves without it). Exact-prefix matches rank first by
population, then names one edit away; a token vocabulary respells one
mistyped word against known place-name tokens ("pest seattle" ->
"west seattle") for retry against the upstream geocoder, which covers
neighbourhood-level places the dump lacks. THERMOGRAPH_CITIES picks the
dump (default cities1000).
- /suggest blends local and upstream results by population with an exactness
boost, so "Seatle" (a Cumbrian hamlet) can't outrank Seattle when the
query is one edit from the city, while "munchen" still surfaces Munich via
upstream (the index only knows English names). Upstream lookups are
memoized and skipped entirely when the index answers convincingly.
- mappicker.js: debounced (250ms) live suggestions with abort + sequence
guards against stale responses, arrow-key navigation, Enter-picks-highlight,
Escape dismissing the list before closing the overlay. Submit goes through
the same typo-tolerant endpoint.
- climate.geocode results now carry population (used for ranking).
2026-07-11 15:36:14 +00:00
|
|
|
// Live suggestions (top 5, single-letter-typo tolerant) as the user types:
|
|
|
|
|
// debounced so a fast typist doesn't fire a request per keystroke, aborted
|
|
|
|
|
// + sequence-guarded so a slow stale response can't overwrite a newer list.
|
|
|
|
|
async function fetchSug(q) {
|
|
|
|
|
const seq = ++sugSeq;
|
|
|
|
|
if (sugAbort) sugAbort.abort();
|
|
|
|
|
sugAbort = new AbortController();
|
2026-07-11 00:29:47 +00:00
|
|
|
try {
|
Typo-tolerant location search suggestions (#33)
Add /api/v2/suggest and wire the location picker's search box to it as a
debounced type-ahead: top-5 place suggestions that tolerate a single-letter
typo (substituted, missing, or extra letter, or two adjacent letters swapped)
anywhere in the query, including the first character.
- backend/places.py: local place index built from a GeoNames cities dump
(downloaded once into data/geonames/, loaded in a background thread; the
app boots and serves without it). Exact-prefix matches rank first by
population, then names one edit away; a token vocabulary respells one
mistyped word against known place-name tokens ("pest seattle" ->
"west seattle") for retry against the upstream geocoder, which covers
neighbourhood-level places the dump lacks. THERMOGRAPH_CITIES picks the
dump (default cities1000).
- /suggest blends local and upstream results by population with an exactness
boost, so "Seatle" (a Cumbrian hamlet) can't outrank Seattle when the
query is one edit from the city, while "munchen" still surfaces Munich via
upstream (the index only knows English names). Upstream lookups are
memoized and skipped entirely when the index answers convincingly.
- mappicker.js: debounced (250ms) live suggestions with abort + sequence
guards against stale responses, arrow-key navigation, Enter-picks-highlight,
Escape dismissing the list before closing the overlay. Submit goes through
the same typo-tolerant endpoint.
- climate.geocode results now carry population (used for ranking).
2026-07-11 15:36:14 +00:00
|
|
|
const res = await fetch(`api/v2/suggest?q=${encodeURIComponent(q)}`, { signal: sugAbort.signal });
|
2026-07-11 00:29:47 +00:00
|
|
|
const d = await res.json();
|
Typo-tolerant location search suggestions (#33)
Add /api/v2/suggest and wire the location picker's search box to it as a
debounced type-ahead: top-5 place suggestions that tolerate a single-letter
typo (substituted, missing, or extra letter, or two adjacent letters swapped)
anywhere in the query, including the first character.
- backend/places.py: local place index built from a GeoNames cities dump
(downloaded once into data/geonames/, loaded in a background thread; the
app boots and serves without it). Exact-prefix matches rank first by
population, then names one edit away; a token vocabulary respells one
mistyped word against known place-name tokens ("pest seattle" ->
"west seattle") for retry against the upstream geocoder, which covers
neighbourhood-level places the dump lacks. THERMOGRAPH_CITIES picks the
dump (default cities1000).
- /suggest blends local and upstream results by population with an exactness
boost, so "Seatle" (a Cumbrian hamlet) can't outrank Seattle when the
query is one edit from the city, while "munchen" still surfaces Munich via
upstream (the index only knows English names). Upstream lookups are
memoized and skipped entirely when the index answers convincingly.
- mappicker.js: debounced (250ms) live suggestions with abort + sequence
guards against stale responses, arrow-key navigation, Enter-picks-highlight,
Escape dismissing the list before closing the overlay. Submit goes through
the same typo-tolerant endpoint.
- climate.geocode results now carry population (used for ranking).
2026-07-11 15:36:14 +00:00
|
|
|
if (seq === sugSeq) renderSug(d.results || []);
|
|
|
|
|
} catch (err) { /* aborted, or offline — the map stays the fallback way to pick */ }
|
|
|
|
|
}
|
|
|
|
|
searchInput.addEventListener("input", () => {
|
|
|
|
|
clearTimeout(sugTimer);
|
|
|
|
|
// The old list stays visible until fresh results land, but its highlight
|
|
|
|
|
// must go — Enter mid-typing shouldn't pick a suggestion for the shorter
|
|
|
|
|
// query the list still reflects.
|
|
|
|
|
const stale = sugEl.querySelector("li.active");
|
|
|
|
|
if (stale) stale.classList.remove("active");
|
|
|
|
|
const q = searchInput.value.trim();
|
|
|
|
|
if (q.length < 2) { sugEl.hidden = true; return; }
|
|
|
|
|
sugTimer = setTimeout(() => fetchSug(q), 250);
|
|
|
|
|
});
|
|
|
|
|
// Arrow keys walk the list (Enter picks via the submit handler below);
|
|
|
|
|
// Escape dismisses the list before the overlay's Escape-to-close kicks in.
|
|
|
|
|
searchInput.addEventListener("keydown", (e) => {
|
|
|
|
|
if (e.key === "Escape" && !sugEl.hidden) {
|
|
|
|
|
sugEl.hidden = true;
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (sugEl.hidden || (e.key !== "ArrowDown" && e.key !== "ArrowUp")) return;
|
|
|
|
|
const items = [...sugEl.children];
|
|
|
|
|
if (!items.length) return;
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const cur = items.findIndex((li) => li.classList.contains("active"));
|
|
|
|
|
const next = e.key === "ArrowDown"
|
|
|
|
|
? (cur + 1) % items.length
|
|
|
|
|
: (cur <= 0 ? items.length - 1 : cur - 1);
|
|
|
|
|
setActiveSug(items, next);
|
|
|
|
|
items[next].scrollIntoView({ block: "nearest" });
|
|
|
|
|
});
|
|
|
|
|
searchForm.addEventListener("submit", (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
clearTimeout(sugTimer);
|
|
|
|
|
// Enter picks the highlighted suggestion when the list is up; otherwise
|
|
|
|
|
// it searches immediately (no debounce wait).
|
|
|
|
|
const active = sugEl.hidden ? null : sugEl.querySelector("li.active");
|
|
|
|
|
if (active) { active.click(); return; }
|
|
|
|
|
const q = searchInput.value.trim();
|
|
|
|
|
if (q) fetchSug(q);
|
2026-07-11 00:29:47 +00:00
|
|
|
});
|
|
|
|
|
// Hide the suggestions when tapping elsewhere in the modal (but not the map,
|
|
|
|
|
// which has its own tap handler).
|
|
|
|
|
overlay.addEventListener("pointerdown", (e) => {
|
|
|
|
|
if (!e.target.closest(".mp-search")) sugEl.hidden = true;
|
|
|
|
|
}, true);
|
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
|
|
|
if (overlay && !overlay.hidden && e.key === "Escape") close();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
Typo-tolerant location search suggestions (#33)
Add /api/v2/suggest and wire the location picker's search box to it as a
debounced type-ahead: top-5 place suggestions that tolerate a single-letter
typo (substituted, missing, or extra letter, or two adjacent letters swapped)
anywhere in the query, including the first character.
- backend/places.py: local place index built from a GeoNames cities dump
(downloaded once into data/geonames/, loaded in a background thread; the
app boots and serves without it). Exact-prefix matches rank first by
population, then names one edit away; a token vocabulary respells one
mistyped word against known place-name tokens ("pest seattle" ->
"west seattle") for retry against the upstream geocoder, which covers
neighbourhood-level places the dump lacks. THERMOGRAPH_CITIES picks the
dump (default cities1000).
- /suggest blends local and upstream results by population with an exactness
boost, so "Seatle" (a Cumbrian hamlet) can't outrank Seattle when the
query is one edit from the city, while "munchen" still surfaces Munich via
upstream (the index only knows English names). Upstream lookups are
memoized and skipped entirely when the index answers convincingly.
- mappicker.js: debounced (250ms) live suggestions with abort + sequence
guards against stale responses, arrow-key navigation, Enter-picks-highlight,
Escape dismissing the list before closing the overlay. Submit goes through
the same typo-tolerant endpoint.
- climate.geocode results now carry population (used for ranking).
2026-07-11 15:36:14 +00:00
|
|
|
function setActiveSug(items, idx) {
|
|
|
|
|
items.forEach((li, i) => li.classList.toggle("active", i === idx));
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 00:29:47 +00:00
|
|
|
function renderSug(list) {
|
Worldwide coverage: grade any point on Earth (#32)
Remove the US+Canada bounding box so every endpoint accepts any lat/lon.
The grading pipeline was already global-ready (ERA5 archive, timezone=auto,
day-of-year climatology), so opening it up is mostly deleting the guard —
plus the edge cases that only exist once the whole globe is in play:
- grid.py: snap() wraps longitude into [-180, 180) and clamps latitude, and
cell centers are normalized so the polar row and the cells straddling the
antimeridian always report valid coordinates to the weather/geocoding APIs.
snap() and from_id() now share one _cell() builder, making id round-trips
exact by construction (verified with a 300k-point global sweep).
- nav.js: neighbor-cell prefetch skips rows past the poles and wraps
longitudes across the dateline instead of sending out-of-range queries.
- Nominatim reverse geocoding requests accept-language=en so place labels
render in one script worldwide (matching the forward geocoder).
- mappicker: search suggestions are no longer filtered to US/CA, the
placeholder and default map view are worldwide.
- calendar: season filter labels flip for southern-hemisphere locations
(Dec-Feb shows as Summer); the underlying month groups are unchanged, so
saved filter selections keep meaning the same months.
Verified end-to-end on a scratch server: Tokyo and Sydney grade with real
labels, a Fiji cell on the antimeridian's east edge builds and serves warm
hits from the derived store, and prefetch=1 on a cold cell still answers
204 without spending weather-API quota.
2026-07-11 15:02:28 +00:00
|
|
|
const shown = list;
|
2026-07-11 00:29:47 +00:00
|
|
|
sugEl.innerHTML = "";
|
|
|
|
|
if (!shown.length) { sugEl.hidden = true; return; }
|
|
|
|
|
shown.forEach((r) => {
|
|
|
|
|
const li = document.createElement("li");
|
|
|
|
|
li.innerHTML = `${r.name}<span class="sub"> — ${[r.admin1, r.country].filter(Boolean).join(", ")}</span>`;
|
|
|
|
|
// A picked search result is an unambiguous choice — drop the pin, recenter,
|
|
|
|
|
// and select it right away (the map is there for manual/fine picks).
|
|
|
|
|
li.onclick = () => {
|
|
|
|
|
sugEl.hidden = true;
|
|
|
|
|
searchInput.value = r.name;
|
|
|
|
|
setPin(r.lat, r.lon, 10);
|
|
|
|
|
finish({ lat: r.lat, lon: r.lon });
|
|
|
|
|
};
|
Typo-tolerant location search suggestions (#33)
Add /api/v2/suggest and wire the location picker's search box to it as a
debounced type-ahead: top-5 place suggestions that tolerate a single-letter
typo (substituted, missing, or extra letter, or two adjacent letters swapped)
anywhere in the query, including the first character.
- backend/places.py: local place index built from a GeoNames cities dump
(downloaded once into data/geonames/, loaded in a background thread; the
app boots and serves without it). Exact-prefix matches rank first by
population, then names one edit away; a token vocabulary respells one
mistyped word against known place-name tokens ("pest seattle" ->
"west seattle") for retry against the upstream geocoder, which covers
neighbourhood-level places the dump lacks. THERMOGRAPH_CITIES picks the
dump (default cities1000).
- /suggest blends local and upstream results by population with an exactness
boost, so "Seatle" (a Cumbrian hamlet) can't outrank Seattle when the
query is one edit from the city, while "munchen" still surfaces Munich via
upstream (the index only knows English names). Upstream lookups are
memoized and skipped entirely when the index answers convincingly.
- mappicker.js: debounced (250ms) live suggestions with abort + sequence
guards against stale responses, arrow-key navigation, Enter-picks-highlight,
Escape dismissing the list before closing the overlay. Submit goes through
the same typo-tolerant endpoint.
- climate.geocode results now carry population (used for ranking).
2026-07-11 15:36:14 +00:00
|
|
|
// Keep one highlight: hovering moves it off whatever the arrows chose.
|
|
|
|
|
li.addEventListener("pointerenter", () =>
|
|
|
|
|
setActiveSug([...sugEl.children], [...sugEl.children].indexOf(li)));
|
2026-07-11 00:29:47 +00:00
|
|
|
sugEl.appendChild(li);
|
|
|
|
|
});
|
Typo-tolerant location search suggestions (#33)
Add /api/v2/suggest and wire the location picker's search box to it as a
debounced type-ahead: top-5 place suggestions that tolerate a single-letter
typo (substituted, missing, or extra letter, or two adjacent letters swapped)
anywhere in the query, including the first character.
- backend/places.py: local place index built from a GeoNames cities dump
(downloaded once into data/geonames/, loaded in a background thread; the
app boots and serves without it). Exact-prefix matches rank first by
population, then names one edit away; a token vocabulary respells one
mistyped word against known place-name tokens ("pest seattle" ->
"west seattle") for retry against the upstream geocoder, which covers
neighbourhood-level places the dump lacks. THERMOGRAPH_CITIES picks the
dump (default cities1000).
- /suggest blends local and upstream results by population with an exactness
boost, so "Seatle" (a Cumbrian hamlet) can't outrank Seattle when the
query is one edit from the city, while "munchen" still surfaces Munich via
upstream (the index only knows English names). Upstream lookups are
memoized and skipped entirely when the index answers convincingly.
- mappicker.js: debounced (250ms) live suggestions with abort + sequence
guards against stale responses, arrow-key navigation, Enter-picks-highlight,
Escape dismissing the list before closing the overlay. Submit goes through
the same typo-tolerant endpoint.
- climate.geocode results now carry population (used for ranking).
2026-07-11 15:36:14 +00:00
|
|
|
// Pre-highlight the top match so Enter picks it straight away.
|
|
|
|
|
setActiveSug([...sugEl.children], 0);
|
2026-07-11 00:29:47 +00:00
|
|
|
sugEl.hidden = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setPin(lat, lon, zoom) {
|
|
|
|
|
pin = { lat, lon };
|
|
|
|
|
if (marker) marker.setLatLng([lat, lon]);
|
2026-07-11 08:10:15 +00:00
|
|
|
else marker = L.marker([lat, lon], pinIcon ? { icon: pinIcon } : undefined).addTo(map);
|
2026-07-11 00:29:47 +00:00
|
|
|
map.setView([lat, lon], zoom || map.getZoom());
|
|
|
|
|
confirmBtn.disabled = false;
|
|
|
|
|
hintEl.textContent = "Pin set — confirm below, or tap elsewhere to move it.";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function finish(p) {
|
|
|
|
|
if (!p) return;
|
|
|
|
|
const cb = onPickCb;
|
|
|
|
|
close();
|
|
|
|
|
if (cb) cb(p.lat, p.lon);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function open(cur, onPick) {
|
|
|
|
|
if (!overlay) build();
|
|
|
|
|
onPickCb = onPick;
|
|
|
|
|
pin = null;
|
|
|
|
|
confirmBtn.disabled = true;
|
Typo-tolerant location search suggestions (#33)
Add /api/v2/suggest and wire the location picker's search box to it as a
debounced type-ahead: top-5 place suggestions that tolerate a single-letter
typo (substituted, missing, or extra letter, or two adjacent letters swapped)
anywhere in the query, including the first character.
- backend/places.py: local place index built from a GeoNames cities dump
(downloaded once into data/geonames/, loaded in a background thread; the
app boots and serves without it). Exact-prefix matches rank first by
population, then names one edit away; a token vocabulary respells one
mistyped word against known place-name tokens ("pest seattle" ->
"west seattle") for retry against the upstream geocoder, which covers
neighbourhood-level places the dump lacks. THERMOGRAPH_CITIES picks the
dump (default cities1000).
- /suggest blends local and upstream results by population with an exactness
boost, so "Seatle" (a Cumbrian hamlet) can't outrank Seattle when the
query is one edit from the city, while "munchen" still surfaces Munich via
upstream (the index only knows English names). Upstream lookups are
memoized and skipped entirely when the index answers convincingly.
- mappicker.js: debounced (250ms) live suggestions with abort + sequence
guards against stale responses, arrow-key navigation, Enter-picks-highlight,
Escape dismissing the list before closing the overlay. Submit goes through
the same typo-tolerant endpoint.
- climate.geocode results now carry population (used for ranking).
2026-07-11 15:36:14 +00:00
|
|
|
// Cancel any in-flight/pending suggestion fetch from the previous open so
|
|
|
|
|
// a late response can't pop a stale list over the fresh empty box.
|
|
|
|
|
clearTimeout(sugTimer);
|
|
|
|
|
sugSeq++;
|
2026-07-11 00:29:47 +00:00
|
|
|
sugEl.hidden = true;
|
|
|
|
|
searchInput.value = "";
|
|
|
|
|
hintEl.textContent = "Search above, or tap the map to drop a pin.";
|
|
|
|
|
overlay.hidden = false;
|
|
|
|
|
|
|
|
|
|
if (!map) {
|
Worldwide coverage: grade any point on Earth (#32)
Remove the US+Canada bounding box so every endpoint accepts any lat/lon.
The grading pipeline was already global-ready (ERA5 archive, timezone=auto,
day-of-year climatology), so opening it up is mostly deleting the guard —
plus the edge cases that only exist once the whole globe is in play:
- grid.py: snap() wraps longitude into [-180, 180) and clamps latitude, and
cell centers are normalized so the polar row and the cells straddling the
antimeridian always report valid coordinates to the weather/geocoding APIs.
snap() and from_id() now share one _cell() builder, making id round-trips
exact by construction (verified with a 300k-point global sweep).
- nav.js: neighbor-cell prefetch skips rows past the poles and wraps
longitudes across the dateline instead of sending out-of-range queries.
- Nominatim reverse geocoding requests accept-language=en so place labels
render in one script worldwide (matching the forward geocoder).
- mappicker: search suggestions are no longer filtered to US/CA, the
placeholder and default map view are worldwide.
- calendar: season filter labels flip for southern-hemisphere locations
(Dec-Feb shows as Summer); the underlying month groups are unchanged, so
saved filter selections keep meaning the same months.
Verified end-to-end on a scratch server: Tokyo and Sydney grade with real
labels, a Fiji cell on the antimeridian's east edge builds and serves warm
hits from the derived store, and prefetch=1 on a cold cell still answers
204 without spending weather-API quota.
2026-07-11 15:02:28 +00:00
|
|
|
map = L.map(mapEl, { worldCopyJump: true, zoomControl: true }).setView([25, 0], 2);
|
Picker map: smaller labels, darker roads; location label coords→place (#27)
* Picker map: smaller city labels, darker/bolder roads (split tile layers)
Split the CARTO Voyager basemap into two raster layers so label size and road
weight are independent: a labels-free base upscaled via tileSize 512 + zoomOffset
-1 (bold, prominent roads) plus a labels-only layer at natural size on top (small,
crisp city names). The darken/saturate filter now targets the base layer only
(.mp-base: brightness .92, contrast 1.12), deepening the roads while leaving label
text at full contrast.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3Q2EkHHnTUX2BfhV5q1Zc
* Location label: show coordinates immediately, then resolve to place name
On selecting a location, write the raw coordinates to the location label right
away, so the user sees the picked spot instantly instead of a stale/blank label.
The existing post-fetch render already overwrites it with the reverse-geocoded
"neighborhood, city, region" string (data.place), so the label now reads
coords → place name live.
- app.js / calendar.js / day.js: set coords in the selection handler before the
fetch; the render/initOnce overwrite is unchanged.
- compare.js: show coordinates through the pending + loading states (instead of
"Locating…") so each chip's coords are replaced live by the place name once
scored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3Q2EkHHnTUX2BfhV5q1Zc
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 08:35:43 +00:00
|
|
|
// Colorful CARTO "Voyager" basemap (no key needed), split into two raster
|
|
|
|
|
// layers so road weight and label size can be tuned independently:
|
|
|
|
|
// • the label-free base is requested one zoom lower and drawn at 512px
|
|
|
|
|
// (tileSize 512 + zoomOffset -1) so roads read bold and prominent;
|
|
|
|
|
// • the labels-only layer is drawn on top at its natural size, keeping city
|
|
|
|
|
// names small and crisp rather than scaled up with the roads.
|
|
|
|
|
// @2x keeps both sharp; a darken/saturate filter (style.css, .mp-base only)
|
|
|
|
|
// deepens the roads while leaving the label text at full contrast.
|
|
|
|
|
const CARTO = "https://{s}.basemaps.cartocdn.com/rastertiles";
|
|
|
|
|
const ATTRIB = '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>';
|
|
|
|
|
L.tileLayer(`${CARTO}/voyager_nolabels/{z}/{x}/{y}@2x.png`, {
|
2026-07-11 08:19:52 +00:00
|
|
|
subdomains: "abcd", maxZoom: 20, tileSize: 512, zoomOffset: -1,
|
Picker map: smaller labels, darker roads; location label coords→place (#27)
* Picker map: smaller city labels, darker/bolder roads (split tile layers)
Split the CARTO Voyager basemap into two raster layers so label size and road
weight are independent: a labels-free base upscaled via tileSize 512 + zoomOffset
-1 (bold, prominent roads) plus a labels-only layer at natural size on top (small,
crisp city names). The darken/saturate filter now targets the base layer only
(.mp-base: brightness .92, contrast 1.12), deepening the roads while leaving label
text at full contrast.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3Q2EkHHnTUX2BfhV5q1Zc
* Location label: show coordinates immediately, then resolve to place name
On selecting a location, write the raw coordinates to the location label right
away, so the user sees the picked spot instantly instead of a stale/blank label.
The existing post-fetch render already overwrites it with the reverse-geocoded
"neighborhood, city, region" string (data.place), so the label now reads
coords → place name live.
- app.js / calendar.js / day.js: set coords in the selection handler before the
fetch; the render/initOnce overwrite is unchanged.
- compare.js: show coordinates through the pending + loading states (instead of
"Locating…") so each chip's coords are replaced live by the place name once
scored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3Q2EkHHnTUX2BfhV5q1Zc
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 08:35:43 +00:00
|
|
|
className: "mp-base", attribution: ATTRIB,
|
|
|
|
|
}).addTo(map);
|
|
|
|
|
L.tileLayer(`${CARTO}/voyager_only_labels/{z}/{x}/{y}@2x.png`, {
|
|
|
|
|
subdomains: "abcd", maxZoom: 20, className: "mp-labels",
|
2026-07-11 00:29:47 +00:00
|
|
|
}).addTo(map);
|
2026-07-11 08:10:15 +00:00
|
|
|
// Branded accent teardrop pin (the same glyph the Find button uses), styled
|
|
|
|
|
// in style.css — replaces Leaflet's default blue raster marker.
|
|
|
|
|
pinIcon = L.divIcon({
|
|
|
|
|
className: "mp-pin", html: PIN_ICON, iconSize: [32, 32], iconAnchor: [16, 30],
|
|
|
|
|
});
|
2026-07-11 00:29:47 +00:00
|
|
|
map.on("click", (e) => setPin(e.latlng.lat, e.latlng.lng));
|
|
|
|
|
}
|
|
|
|
|
// Leaflet measures the container on creation; it's zero-sized while hidden, so
|
|
|
|
|
// recalc once the modal is actually visible and (if reopening on a known spot)
|
|
|
|
|
// drop a pin there as the starting point.
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
map.invalidateSize();
|
|
|
|
|
if (cur && cur.lat != null && cur.lon != null) setPin(cur.lat, cur.lon, 10);
|
|
|
|
|
searchInput.focus();
|
|
|
|
|
}, 60);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function close() { if (overlay) overlay.hidden = true; }
|
|
|
|
|
|
|
|
|
|
window.LocationPicker = { open, PIN_ICON };
|
|
|
|
|
})();
|