What changed, and why
Typing some names ("Jesus", "John F Kennedy") sometimes produced no autocomplete at all, and "Alfred Nobel" matched only near the end of the name. Probed live, the Wikipedia title-search endpoint answers every one of those queries — including "John F Kennedy" without the period. The real failure sat in our seam: a Wikipedia 429 or timeout made the search return [], byte-for- byte identical to a genuine "no matches". Nothing retried at any layer, so one blip meant the dropdown silently never opened. The cluster's shared egress IP sees such blips in bursts, and the in-app suggester's own wiki traffic competes from the same pod.
This is the same failure class PR #34 fixed in grounding, applied to search: a transient miss must stay distinguishable from a definitive answer. The change is small — the search outcome gains a transient flag, the server retries once, and the combobox quietly retries once more instead of treating weather as an answer. Worst case is exactly the old behavior; the typical blip now recovers within a second without the player doing anything.
The search outcome learns to tell weather from absence
searchFigures now returns { results, transient } instead of a bare array. One fetch attempt is factored out (fetchSearch, null when the endpoint didn't answer); a null first attempt gets one spaced retry, 250ms later. Only a real answer — including a legitimate empty one — is ever cached; a failure that survives the retry comes back transient: true and uncached, so the next keystroke gets a fresh shot.
The outcome type, the single-attempt helper, and the retry + cache-real-answers-only flow.
server/utils/figure-search.ts · 110 lines
⋯ 21 lines hidden (lines 1–21)
⋯ 31 lines hidden (lines 29–59)
⋯ 1 line hidden (lines 81–81)
⋯ 1 line hidden (lines 110–110)
The combobox treats a transient empty as weather
The route passes the flag through untouched. In the picker, runSearch now refuses to let a transient empty answer mean anything: it keeps whatever results are already on screen (a miss mid-type no longer blanks the list) and schedules one quiet retry 800ms later. The retry reuses the existing safety rails — it lands on the same seq guard that drops stale responses, and a new keystroke clears the timer because it lives in the same searchDebounce slot.
One added branch: transient + empty → keep what's showing, retry once.
components/FigurePicker.vue · 387 lines
⋯ 254 lines hidden (lines 1–254)
⋯ 110 lines hidden (lines 278–387)
Verification
npx vitest run green (350 tests), npx nuxt typecheck clean, 12/12 Playwright e2e. The new server tests pin: a 429 recovered by the retry (figure-search.spec.ts:69), a surviving failure flagged transient and never cached (:84, :100), and a real empty answer caching as non-transient (:93). The new component tests pin the quiet retry landing matches without a keystroke and the never-blank-on-weather behavior — both routed around the grounding watcher that shares the $fetch stub.