EveryPlace
A travel log for two people: flights, hotel stays and visited cities, together and apart. Twenty-five years of history extracted by Gemini from confirmation emails, PDFs and boarding passes, then rendered as stats, a globe and a procedurally stamped passport. Personal tool, not a product.
- 2026
- Design, engineering
- TypeScript, Next.js, Tailwind CSS, Drizzle, Neon Postgres, deck.gl, MapLibre, Gemini, Vitest

Twenty-five years of flights, hotel stays and cities for two people, with two goals in that order: get the history in without it being torture, and then make it look good.
Getting data in
Half the project is the ingest pipeline, and it is designed on the assumption that the model will be wrong, so that being wrong is cheap. Three lanes converge on one Zod contract, one resolver and one confirmation queue.
Documents the airline issued go through a deterministic parser. A .pkpass file is unzipped in memory and its boarding-pass barcode (the IATA BCBP format) is read positionally: airline, flight number, origin, destination, seat, julian date. A barcode string pasted as text is recognised by its shape and read the same way. Nothing in this lane touches a model, and it enters the queue with full confidence.
Everything else goes through Gemini. Paste the text of a confirmation email, upload the PDF the hotel sent, or drop a photo of a paper ticket: the model reads it directly, multimodal, with no OCR step in front. One email with an outbound flight, a return and a hotel comes back as three records of mixed types, classified as flight, ride, stay or visit. A ferry or bus ticket is never a flight, even when it carries a booking code and a cabin class; the first PDF that broke that rule was a Buquebus to Colonia, which came out as six flights between airports that do not exist.
The model returns names, never IATA codes or IDs, because it is confident and wrong about small airports. It also never guesses in silence: “summer 2011” comes back as a null date with a hint, “March 2015” as a month with no day, and every field it could not determine is listed as unresolved. Zod validates the output against fixtures captured from real responses, malformed ones included.
Resolution is plain TypeScript. Exact IATA or ICAO code, then normalized name, then a trigram fuzzy match in Postgres. Anything under 0.6 similarity, or a near tie, stays unresolved with its candidates offered as a dropdown in the queue. Passenger names preselect travelers as a suggestion, never an assignment. A record cannot be confirmed without at least one traveler on it.
Batches are deduped by SHA-256 of the input, and the original text or binary is kept in the database so a batch can be reprocessed after the prompt improves. Calls to Gemini retry with exponential backoff; when they run out, the batch is marked failed with a retry button, and the input is already saved.
What it shows
Individual stats: coverage of the 195 countries and six continents, distance as laps of the Earth and the way to the Moon, records like the northernmost airport and the farthest point from home, streaks and droughts, top routes and airlines, series by year. A comparative view with a geographic Venn, shared milestones such as the first trip together, and the balance between the two travelers. Trips are derived from the data by grouping flights, stays and visits; there is no trip entity to maintain.
The map is one deck.gl pipeline with two views. Flat uses MapLibre tiles; the globe uses country polygons over an opaque sphere, because tiles do not work there. Routes are great-circle arcs sampled with spherical interpolation and drawn as paths, with unwrapped longitudes so a trans-Pacific flight crosses the antimeridian instead of circling the map. Edge width is proportional to times flown, colour by traveler with a blend for shared flights. When WebGL is unavailable the same sampling feeds a static SVG.
The passport is procedural SVG. Each stamp’s seed derives from country, arrival date, city and traveler, and from it come the border shape, a rotation between -18 and 18 degrees, irregular ink density with gaps in the stroke, and a two- or three-ink palette per country. Deterministic on purpose: the same country visited in 2011 and 2019 gives two different stamps of the same family, and reloading changes nothing.

Technology
- Next.js 15 App Router with Server Components reading Neon Postgres directly through the serverless HTTP driver. No ISR, no cache layer: at a few hundred records the aggregations run in tens of milliseconds and precomputing buys nothing.
- Drizzle ORM with versioned migrations applied explicitly, never pushed from the schema. Seed tables for airports, airlines and cities come from OpenFlights, country polygons from Natural Earth simplified with mapshaper, plus a hand-curated list of aircraft types.
- Gemini through Google AI Studio as the only model, called multimodally with a Zod schema as the output contract. Always mocked in tests, never called live.
- deck.gl and MapLibre GL for the flat map and the globe, loaded client-side only since neither supports server rendering.
- jose for the single shared password session; the whole site sits behind it, so there is no public link.
- Vitest, 362 tests across 27 files, focused on the places where a silent bug corrupts data or breaks the visuals: the haversine math, the ingest resolver against dirty real-world fixtures, and the stamp generator’s determinism.
- Vercel for hosting, with Tailwind CSS 4 for styling.