Skip to content

feat(lua): geocode cache lib (ttymap.geocode) — forward + reverse with spatial bucketing #338

Description

@Kohei-Wada

Motivation

Three bundled plugins independently hit Nominatim / Wikipedia for "lat/lon ↔ name" lookups, none of which cache effectively:

  • info.lua — reverse geocodes the map centre every 5s via Nominatim's /reverse. URL contains the live lat/lon, so any pan-and-pan-back re-fetches even though the answer for "Tokyo" hasn't changed.
  • search/init.lua (forward) — Nominatim /search?q=.... Re-issuing the same query (e.g. opening the palette, searching "Tokyo", closing, re-opening, searching "Tokyo" again) re-fetches.
  • wiki/init.lua — Wikipedia geosearch by lat/lon. Same micro-pan problem as info.
  • (traceroute already uses fetch_cached — IP→geo is one URL per host, so URL-keyed caching is fine there.)

ttymap.http:fetch_cached(url, ttl) already exists as a URL-keyed HTTP-level disk cache, but URL-keying is the wrong granularity for spatial queries: every 0.0001° camera move makes a new URL, every "Tokyo " vs " tokyo" makes a new URL.

Nominatim's terms of service explicitly require local caching + max 1 req/s. Fixing this is overdue.

Proposal

A bundled lib at runtime/lua/ttymap/geocode.lua that does semantic caching on top of ttymap.storage + ttymap.http:

```lua
local geo = require "ttymap.geocode"

-- Forward: query string → coordinates + canonical display name
geo.forward("Tokyo Tower", function(lat, lon, display_name, source)
-- source ∈ "memory" | "storage" | "network"
end)

-- Reverse: coordinates → canonical place name
geo.reverse(35.6586, 139.7454, function(display_name, source) end)

-- Sync cache reads
local lat, lon, name = geo.cached_forward("Tokyo Tower")
local name = geo.cached_reverse(35.6586, 139.7454)
```

Internals

  • Forward keying: `normalize(query)` — trim + lowercase + collapse whitespace. So "Tokyo", " Tokyo ", "tokyo" share an entry.
  • Reverse keying: spatial bucketing — quantize lat/lon to a coarser grid (default ~0.001° ≈ 100m at the equator, configurable per-call). `cache_key = string.format("%.3f_%.3f", lat, lon)`. Means any pan within the bucket hits cache; the existing 5s throttle in info.lua becomes redundant.
  • Persistence: `ttymap.storage:open("geocode")`, two key namespaces. Forward entries: `fwd_.json`; reverse: `rev_.json`. Atomic write via storage's existing primitive.
  • Coalescing: concurrent calls for the same key fold onto a single in-flight job, same pattern as feat(lua): shared user-location cache (ttymap.location lib) #307 / ttymap.location.
  • Endpoints configurable via `require("ttymap.geocode").forward_endpoint` / `.reverse_endpoint` — defaults pointing at Nominatim, but a user can swap to a self-hosted instance.
  • TTL: long (or none) by default. Place names don't change. Honour standard `ttl_seconds` field on the config table for users who want it; default infinity.

Eviction / unbounded growth

Realistically: even an active user generates maybe O(1000) unique geocode entries lifetime. Each is a small JSON blob. Total disk usage is negligible. Punt eviction until someone reports a problem. `ttymap.storage` doesn't have an LRU primitive anyway, and adding one is a separate feature (see also #296 for tile cache eviction).

Migrations

In the same PR (once the lib lands):

  1. `info.lua` — drop the 5s throttle + manual fetch; replace with `geo.reverse(lat, lon, cb)`. Bucketing replaces the time-based throttle.
  2. `search/init.lua` — replace `ttymap.http:fetch(nominatim.url(query))` with `geo.forward(query, cb)`. The async palette provider drains the lib's cb instead of polling a raw job.
  3. `wiki/init.lua` — `geo.reverse(lat, lon, cb)` for the place-name lookup; the geosearch itself stays as raw HTTP (it's not a 1:1 lat/lon→string lookup, returns N nearby articles).

Out of scope

  • LRU eviction — defer until reported as a real problem.
  • Server-side cache invalidation — TTL-only; users with `refresh`-like needs can clear the namespace via `ttymap.storage:open("geocode"):delete(key)` manually.
  • Bulk geocoding API — single-key only; if a plugin needs N lookups it just calls N times and the lib coalesces.
  • Engine-level geocoding service — same convention as feat(lua): shared user-location cache (ttymap.location lib) #307: Lua lib, no Rust changes.

Refs

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions