@@ -27,6 +27,7 @@ import { ElevationProfile } from '@/components/ElevationProfile';
2727import pkg from '@/package.json' ;
2828import { haversineM , toSemicircles } from '@/lib/geometry' ;
2929import { shareOrDownloadGpx } from '@/lib/gpxShare' ;
30+ import { missingTiles as missingRoadTiles , bboxForTiles as roadBboxForTiles , tileKey as roadTileKey } from '@/lib/roadTiles' ;
3031
3132export default function Home ( ) {
3233 const [ bbox , setBbox ] = useState < { south : number ; west : number ; north : number ; east : number } | null > ( null ) ;
@@ -106,6 +107,8 @@ export default function Home() {
106107 const clickChainRef = useRef < Promise < void > > ( Promise . resolve ( ) ) ;
107108 const generateAbortControllerRef = useRef < AbortController | null > ( null ) ;
108109 const roadsAbortControllerRef = useRef < AbortController | null > ( null ) ;
110+ const fetchedRoadTilesRef = useRef < Set < string > > ( new Set ( ) ) ;
111+ const roadTileCacheRef = useRef < [ number , number ] [ ] [ ] > ( [ ] ) ;
109112 const pointsRef = useRef < { lat : number ; lon : number ; id : string ; status ?: 'pending' | 'snapped' } [ ] > ( [ ] ) ;
110113 const manualRouteRef = useRef < [ number , number ] [ ] [ ] > ( [ ] ) ;
111114 const historyRef = useRef < { points : { lat : number ; lon : number ; id : string ; status ?: 'pending' | 'snapped' } [ ] , route : [ number , number ] [ ] [ ] , selectionBoxes : { north : number ; south : number ; east : number ; west : number } [ ] , preAreaPointCount : number | null } [ ] > ( [ ] ) ;
@@ -363,25 +366,46 @@ export default function Home() {
363366 useEffect ( ( ) => {
364367 if ( ! bbox ) return ;
365368
369+ // Tile the viewport (plus a small buffer) at a coarse grid so panning only
370+ // fetches the newly-revealed tiles instead of re-downloading the whole
371+ // viewport's road geometry every time — this is the single biggest source
372+ // of Vercel "Fast Origin Transfer" usage, since /api/roads fires on every
373+ // pan/zoom (moveend).
374+ const missing = missingRoadTiles ( bbox , fetchedRoadTilesRef . current ) ;
375+
376+ if ( missing . length === 0 ) {
377+ // Every tile in view has already been fetched — reuse the accumulated
378+ // cache, no network request needed.
379+ if ( roadTileCacheRef . current . length > 0 ) setAllRoads ( roadTileCacheRef . current ) ;
380+ return ;
381+ }
382+
366383 roadsAbortControllerRef . current ?. abort ( ) ;
367384 roadsAbortControllerRef . current = new AbortController ( ) ;
368385 const signal = roadsAbortControllerRef . current . signal ;
369386
387+ // Fetch only the bounding box covering the missing tiles, not the full viewport.
388+ const fetchBbox = roadBboxForTiles ( missing ) ;
389+
370390 const timer = setTimeout ( ( ) => {
371391 fetch ( '/api/roads' , {
372392 method : 'POST' ,
373393 headers : { 'Content-Type' : 'application/json' } ,
374- body : JSON . stringify ( { bbox } ) ,
394+ body : JSON . stringify ( { bbox : fetchBbox } ) ,
375395 signal
376396 } )
377397 . then ( res => res . json ( ) )
378398 . then ( data => {
379399 if ( data . roads ) {
380- console . log ( `[StreetSweep] Received ${ data . roads . length } roadmap segments.` ) ;
400+ console . log ( `[StreetSweep] Received ${ data . roads . length } roadmap segments for ${ missing . length } new tiles .` ) ;
381401 // Keep the previous road set if this fetch came back empty
382402 // (transient Overpass hiccup) so ridden-road snapping doesn't
383403 // fall back to raw GPS traces.
384- if ( data . roads . length > 0 ) setAllRoads ( data . roads ) ;
404+ if ( data . roads . length > 0 ) {
405+ for ( const t of missing ) fetchedRoadTilesRef . current . add ( roadTileKey ( t ) ) ;
406+ roadTileCacheRef . current = roadTileCacheRef . current . concat ( data . roads ) ;
407+ setAllRoads ( roadTileCacheRef . current ) ;
408+ }
385409 setServiceWarning ( ! ! data . degraded ) ;
386410 }
387411 } )
0 commit comments