← Back to portfolio

Wayfarer

Wikidata-Grounded Map Query Tool — Case Study

wayfarer.wjleece.dev — live, BYOK (bring your own Anthropic or OpenAI key)
ReactTypeScriptViteNode.js / ExpressWikidata SPARQLLeafletClaude & GPT-5.6Docker

The Problem

I wanted a way to turn a plain-English question, something like "South American capitals" or "Beatles birthplaces", into a map. The two obvious approaches both fall short: hand-writing SPARQL against Wikidata for every query shape doesn't scale, and trusting an LLM's raw output means it sounds just as confident whether or not it's actually right. Most "ask an LLM to find some places" demos skip that second half of the problem. They show you a pin and never tell you whether it's real.

Architecture & Tech Decisions

Wikidata is the source of truth; the LLM is only a narrow translator and fallback. Given that, a smaller/cheaper model is viable and the critical path is the quality of SPARQL generation and entity disambiguation, not raw model horsepower.

Schema-discovery agent

Instead of a fixed library of query templates, the agent reads Wikidata's own schema at query time: it searches for the relevant properties and classes, probes how real instances are modelled, and builds and tests a SPARQL query live against Wikidata before it's ever proposed as an answer. The same code path handles "South American capitals" and "WWI Western Front battlefields" with no query-type branch. A single-point capital and a multi-point battlefield list go through identical logic.

Verification gate

Before a result reaches the map it's checked along independent axes: does each row match the expected type, does the row count look sane, does a second, independent check on a sample of results hold up. A failed check triggers one bounded repair pass. If the query still can't be trusted, the result is downgraded rather than dropped, so nothing vanishes silently.

Two-axis trust model

Every pin carries two independent trust labels: coordinate trust (verified from Wikidata's own data → geocoded → approximate) and membership trust (confirmed by a real Wikidata relationship vs. the model's own unverified claim). A pin only reads as fully clean when both are at their strongest tier; anything weaker is visibly flagged in the UI rather than blended in with verified results.

Multi-provider BYOK

No server-side LLM key. Users bring their own Anthropic or OpenAI key, entered in the browser and sent per request via header, never logged or stored server-side. The API layer's provider dispatch is a single function keyed off a model-id prefix, so adding OpenAI's GPT-5.6 line-up alongside the original Claude integration was a same-day change rather than a rewrite.

Fallback, not failure

If Wikidata has no structured answer for a query, usually a subjective or very recent topic, the app falls back to curated model knowledge instead of returning nothing. Every result from that path is honestly labelled "model-suggested", never presented as grounded.

Cost control & deployment

A cross-user result cache (24h TTL, keyed on query and model) serves repeat queries for free, since the underlying Wikidata data barely changes day to day. The app ships as two Docker containers, an Express API and an nginx-served Vite build, behind a Caddy reverse proxy.

Outcome

Live at wayfarer.wjleece.dev. The canonical test case, "South American capitals", returns every capital, none spurious, each with a verified coordinate and a working source link. Harder multi-entity queries, like every member of a band's birthplace, run through the identical pipeline with no special-casing. The whole thing is BYOK: no account, no server-side spend, just your own Anthropic or OpenAI key entered in the browser.