OdoKeep è ora su App Store. Scaricala gratis.

Tutti gli articoli

Dati geografici

Sostituire le query geospaziali con file JSON e Cloudflare R2

Una griglia condivisa, JSON statici su Cloudflare R2 e una cache che distingue dati mancanti da una mappa vuota.

Aggiornato il

In questo articolo

Ogni persona che cercava prezzi del carburante vicini in OdoKeep poneva una domanda diversa agli stessi dati.

Cambiavano posizione, raggio e carburante. L'elenco delle stazioni era comune e veniva ricostruito ogni giorno.

Era sufficiente per ripensare dove eseguire la ricerca.

OdoKeep è un diario dei veicoli in React Native. La funzione prezzi usa fonti nazionali di Portogallo, Spagna, Francia, Italia e Austria. Il modulo delle celle documenta circa 46.000 stazioni, un numero variabile con le fonti. Inizialmente l'app chiedeva al backend le stazioni vicine e i conteggi dei marchi per paese.

Ora legge JSON da Cloudflare R2 tramite un dominio pubblico personalizzato. Supabase gestisce ancora i dati privati del garage. Il processo di pubblicazione legge anche i metadati di attribuzione e comunica il proprio stato. È venuta meno la necessità di rispondere dal database a ogni ricerca di prezzi.

Avrei potuto pubblicare un file per paese e filtrarlo sul telefono. Sarebbe stato semplice, ma una ricerca vicina non dovrebbe scaricare tutte le stazioni nazionali. Serviva una suddivisione coerente con l'uso della schermata.

Una griglia condivisa tra pubblicazione e telefono

La partizione usa una griglia di mezzo grado. lib/fuel-prices/tiles.ts contiene il calcolo usato da entrambi:

const TILE_DEGREES = 0.5;

function tileKey(lat: number, lon: number): string {
  return `${Math.floor(lat / TILE_DEGREES)}_${Math.floor(lon / TILE_DEGREES)}`;
}

Lisbona, a 38.72, -9.14, appartiene a 77_-19. La longitudine negativa rende importante Math.floor: troncare assegnerebbe i punti a ovest di Greenwich alla cella sbagliata.

La struttura pubblicata si descrive in poche righe:

fuel/
  index.json
  sources.json
  brands/
    PT.json
    ES.json
    ...
  tiles/
    77_-19.json
    77_-18.json
    ...

Ogni cella contiene coordinate, identità, indirizzo, prezzi, eventuali orari e tempi di aggiornamento delle fonti. Non contiene la distanza dall'utente: la distanza appartiene a una ricerca, non alla stazione.

Scaricare un rettangolo e filtrare per distanza

Il client calcola le celle che coprono il rettangolo circoscritto al cerchio cercato. L'intervallo di latitudine deriva approssimativamente dal raggio diviso per i chilometri per grado. Quello di longitudine viene ampliato in base al coseno della latitudine, perché i gradi di longitudine si restringono verso nord.

Il rettangolo è volutamente impreciso. Può includere una cella d'angolo senza stazioni nel cerchio. Dopo il caricamento, la distanza haversine scarta quelle fuori raggio e ordina le altre. Il raggio terrestre resta quello della query sostituita.

Il numero di file giustifica la dimensione scelta. Mezzo grado è alto circa 56 km, con larghezza minore alle latitudini servite. Il raggio standard di 10 km ha diametro 20 km. I test campionano la copertura documentata e verificano un massimo di quattro celle per quel raggio e nove per il limite di 30 km.

È facile dimenticare il diametro. «Una ricerca di 30 km entra in una cella di 56 km» sembra plausibile finché non si disegna il cerchio.

Una griglia più larga ridurrebbe il numero massimo di richieste, ma imporrebbe file maggiori per la ricerca comune. La scelta attuale usa più richieste sui raggi grandi per scaricare pezzi piccoli su quelli piccoli.

L'indice rende economiche le zone vuote. Elenca solo le celle popolate e contiene dimensione della griglia e data di generazione. Una ricerca costiera elimina le celle note come vuote prima di richiederle. Il client rifiuta un indice con una griglia diversa invece di interpretarne male le chiavi.

Anche i conteggi per marchio vengono costruiti una volta per paese. Cambiare marchio non richiede più un'aggregazione nazionale nel database.

Dopo il caricamento, cambiare carburante, marchio o raggio può riutilizzare le stazioni già disponibili. La rete non deve partecipare a ogni modifica della vista.

Vuoto e fallito sono risultati diversi

La cache considera valide le voci per sei ore. Mantiene dati decodificati in memoria, copie persistenti per gli avvii successivi e una promessa in corso per chiave. Due consumatori simultanei condividono quindi la richiesta. Se la rete fallisce, una vecchia copia utilizzabile può ancora servire la ricerca.

La scelta centrale è distinguere tre esiti:

type TileOutcome =
  | { status: "ok"; stations: TileStation[] }
  | { status: "empty" }
  | { status: "failed" };

Una cella vuota e una che non sono riuscito a scaricare non forniscono la stessa prova.

Supponiamo che una ricerca attraversi due celle popolate. Una risponde, l'altra restituisce 503 senza cache utilizzabile. Mostrare solo la prima restringerebbe silenziosamente la zona cercata. Quella lista potrebbe poi essere ordinata e salvata come risposta completa.

stationsAround fallisce quando manca qualsiasi cella necessaria. Il livello superiore può usare la cache di una ricerca precedente o spiegare che i dati non sono disponibili. Non trasforma una parte mancante della mappa in un risultato completo.

Il trattamento attuale di un 404 è più limitato: equivale a vuoto e viene ricordato per cinque minuti. La durata breve conta durante la pubblicazione, quando una cella può mancare temporaneamente. L'indice evita molte richieste simili, ma un 404 non offre la certezza di un insieme versionato e coerente.

Pubblicare tutti i dati, prima le celle

Il generatore TypeScript richiede tutti i paesi configurati e rifiuta la pubblicazione se un adattatore fallisce. Altrimenti un guasto di una fonte diventerebbe un nuovo insieme da cui un paese è sparito. Conservare l'ultima versione completa è preferibile.

La GitHub Action pianificata usa Bun. Il percorso di generazione impiega moduli integrati e sorgenti del progetto, senza installare l'intero albero di dipendenze dell'app mobile. Produce una directory; il caricamento è un passaggio separato.

I JSON vengono compressi e serviti con il corretto content encoding. gzip -n omette il timestamp nell'intestazione compressa, così un input identico non cambia byte solo perché compresso un altro giorno. Gli oggetti hanno una direttiva di cache pubblica di un'ora.

Le celle vengono caricate prima dell'indice. Pubblicare l'indice per primo annuncerebbe chiavi che un caricamento fallito potrebbe non fornire mai.

Questo ordine non rende atomico il rilascio. I file vengono sostituiti sul posto e un client può vedere generazioni miste. La pulizia può eliminare una vecchia cella mentre qualcuno conserva il precedente indice. Per questa consultazione dei prezzi accetto la coerenza limitata e la breve memoria dei file mancanti. Un insieme che richiedesse un'istantanea coerente avrebbe bisogno di percorsi per generazione e di un puntatore cambiato solo dopo aver caricato tutto.

Quando questa architettura è adatta

R2 è utile perché il listino pubblicato non addebita la banda in uscita verso internet. Storage e operazioni fanno comunque parte del modello di costo R2, e la cache va configurata. Un dominio personalizzato può usare la cache Cloudflare, come spiega la documentazione dei bucket pubblici. Non ho una fattura prima e dopo da allegare: il cambiamento concreto è che le ricerche non consumano più letture o traffico in uscita dal database.

Una pubblicazione quotidiana non rende i prezzi aggiornati al minuto. L'orario della fonte resta separato e l'attribuzione accompagna i dati. Le fonti nazionali hanno frequenze e condizioni di riutilizzo diverse. Servire un file non le cambia.

Funziona per dati pubblici, condivisi, geograficamente delimitati e letti molto più spesso di quanto cambino. Sceglierei diversamente per dati privati, autorizzazione per utente o prezzi in tempo reale dipendenti da ogni transazione.

Il telefono conosceva già posizione, raggio e filtri. Una porzione locale riutilizzabile gli permette di completare la ricerca senza chiedere al server di riscoprire le stesse stazioni.

Puoi provare il browser dei prezzi in OdoKeep, insieme al diario dei veicoli che supporta. L'app iOS permette di provarlo lungo i tuoi percorsi.