EveryPlace
Una bitácora de viajes para dos personas: vuelos, estadías y ciudades visitadas, juntos y por separado. Veinticinco años de historia extraídos por Gemini desde mails de confirmación, PDFs y pases de abordar, y mostrados como estadísticas, un globo y un pasaporte con sellos procedurales. Herramienta personal, no un producto.
- 2026
- Diseño, desarrollo
- TypeScript, Next.js, Tailwind CSS, Drizzle, Neon Postgres, deck.gl, MapLibre, Gemini, Vitest

Veinticinco años de vuelos, estadías y ciudades para dos personas, con dos objetivos en ese orden: meter la historia sin que sea una tortura, y después hacer que se vea bien.
Cómo entran los datos
La mitad del proyecto es el pipeline de ingesta, y está diseñado asumiendo que el modelo se va a equivocar, para que equivocarse sea barato. Tres carriles convergen en un contrato Zod, un resolvedor y una cola de confirmación.
Los documentos que emitió la aerolínea pasan por un parser determinista. Un archivo .pkpass se descomprime en memoria y su código de barras (el formato IATA BCBP) se lee por posición: aerolínea, número de vuelo, origen, destino, asiento, día juliano. Un código de barras pegado como texto se reconoce por su forma y se lee igual. Nada en este carril toca un modelo, y entra a la cola con confianza total.
Todo lo demás pasa por Gemini. Pegás el texto de un mail de confirmación, subís el PDF que mandó el hotel o dejás una foto de un pasaje de papel: el modelo lo lee directo, multimodal, sin un paso de OCR adelante. Un mail con un vuelo de ida, la vuelta y un hotel vuelve como tres registros de tipos distintos, clasificados como vuelo, traslado, estadía o visita. Un pasaje de ferry u ómnibus nunca es un vuelo, aunque traiga código de reserva y clase de cabina; el primer PDF que rompió esa regla fue un Buquebus a Colonia, que salió como seis vuelos entre aeropuertos que no existen.
El modelo devuelve nombres, nunca códigos IATA ni IDs, porque se equivoca con seguridad en los aeropuertos chicos. Tampoco adivina en silencio: “verano de 2011” vuelve como fecha nula con una pista, “marzo de 2015” como un mes sin día, y cada campo que no pudo determinar queda listado como sin resolver. Zod valida la salida contra fixtures capturados de respuestas reales, incluidas las malformadas.
La resolución es TypeScript común. Código IATA o ICAO exacto, después nombre normalizado, después una búsqueda difusa por trigramas en Postgres. Todo lo que queda por debajo de 0.6 de similitud, o en empate cercano, sigue sin resolver con sus candidatos ofrecidos en un desplegable dentro de la cola. Los nombres de pasajeros preseleccionan viajeros como sugerencia, nunca como asignación. Un registro no se puede confirmar sin al menos un viajero.
Los lotes se deduplican por SHA-256 de la entrada, y el texto o binario original se conserva en la base para poder reprocesar un lote cuando el prompt mejora. Las llamadas a Gemini reintentan con backoff exponencial; cuando se agotan, el lote queda marcado como fallido con un botón de reintento, y la entrada ya está guardada.
Qué muestra
Estadísticas individuales: cobertura de los 195 países y seis continentes, distancia en vueltas a la Tierra y camino a la Luna, récords como el aeropuerto más al norte y el punto más lejano de casa, rachas y sequías, rutas y aerolíneas más frecuentes, series por año. Una vista comparativa con un Venn geográfico, hitos compartidos como el primer viaje juntos, y el balance entre los dos viajeros. Los viajes se derivan de los datos agrupando vuelos, estadías y visitas; no hay una entidad viaje que mantener.
El mapa es un solo pipeline de deck.gl con dos vistas. La plana usa tiles de MapLibre; el globo usa polígonos de países sobre una esfera opaca, porque ahí los tiles no funcionan. Las rutas son arcos de círculo máximo muestreados con interpolación esférica y dibujados como trazos, con longitudes sin envolver para que un vuelo transpacífico cruce el antimeridiano en vez de dar la vuelta al mapa. El grosor de cada arista es proporcional a las veces voladas, el color por viajero con una mezcla para los vuelos compartidos. Cuando no hay WebGL, el mismo muestreo alimenta un SVG estático.
El pasaporte es SVG procedural. La semilla de cada sello deriva de país, fecha de llegada, ciudad y viajero, y de ella salen la forma del borde, una rotación entre -18 y 18 grados, densidad de tinta irregular con huecos en el trazo y una paleta de dos o tres tintas por país. Determinista a propósito: el mismo país visitado en 2011 y 2019 da dos sellos distintos de la misma familia, y recargar no cambia nada.

Tecnología
- Next.js 15 App Router con Server Components que leen Neon Postgres directo por el driver HTTP serverless. Sin ISR, sin capa de caché: con unos cientos de registros las agregaciones corren en decenas de milisegundos y precomputar no compra nada.
- Drizzle ORM con migraciones versionadas aplicadas de forma explícita, nunca empujadas desde el esquema. Las tablas semilla de aeropuertos, aerolíneas y ciudades vienen de OpenFlights, los polígonos de países de Natural Earth simplificados con mapshaper, más una lista curada a mano de modelos de avión.
- Gemini vía Google AI Studio como único modelo, llamado en modo multimodal con un esquema Zod como contrato de salida. Siempre mockeado en tests, nunca llamado en vivo.
- deck.gl y MapLibre GL para el mapa plano y el globo, cargados solo en cliente porque ninguno soporta renderizado en servidor.
- jose para la sesión de contraseña única compartida; todo el sitio está detrás, así que no hay enlace público.
- Vitest, 362 tests en 27 archivos, enfocados en los lugares donde un bug silencioso corrompe datos o rompe lo visual: la matemática de haversine, el resolvedor de ingesta contra fixtures reales y sucios, y el determinismo del generador de sellos.
- Vercel como hosting, con Tailwind CSS 4 para los estilos.