system-design52 min read

How to Design Maps & Navigation System like Google Maps — A Senior+ Guide | Ayodhyya

How to Design Maps & Navigation System like Google Maps

Building real-time navigation, geo-indexing, and route optimization at global scale — A Senior+ System Design Guide

By Ayodhyya  |  July 14, 2026  |  45 min read  |  System Design

1. Introduction — Why Google Maps Matters

Google Maps is one of the most complex and widely used distributed systems ever built. With over 1 billion monthly active users, 5 billion+ places indexed worldwide, 20+ petabytes of map data, and real-time traffic information covering 220+ countries and territories, it represents the pinnacle of geospatial engineering at planetary scale.

1B+
Monthly Active Users
5B+
Places Indexed
20+ PB
Map Tile Data
220+
Countries Covered
100M+
km of Roads Mapped
25B+
Map Requests/Day

Every time you open Google Maps, you trigger an intricate orchestration of tile rendering servers, geospatial index lookups, routing engines, traffic prediction models, and CDN edge caching — all happening within under 100 milliseconds. The system handles zoom levels from a street-level view of a single building to a full view of Earth, seamlessly stitching together satellite imagery, road networks, business listings, real-time traffic overlays, and 3D terrain data.

Google Maps isn't just about showing you a map. It predicts your ETA with astonishing accuracy, reroutes you around accidents in real-time, shows you crowdedness levels at restaurants, lets you virtually walk through streets with Street View, and even helps you find parking spots. It processes billions of GPS data points daily from Android devices and taxis to fuel its traffic engine — all while maintaining privacy guarantees and data freshness.

In this deep-dive system design article, we will dissect every major component of a Google Maps-like system. We'll cover the data models that power place lookups, the geospatial indexing structures that enable sub-millisecond coordinate searches, the routing algorithms that compute optimal paths across continental road networks, the tile rendering pipeline that serves billions of map images daily, and the real-time traffic infrastructure that processes millions of GPS pings per second. This is a comprehensive guide designed for senior+ engineers preparing for system design interviews or architecting geospatial systems in production.

What You'll Learn:
  • How to model roads, places, and coordinates at global scale
  • Geospatial indexing with Geohash, QuadTree, and Google S2
  • Vector and raster tile rendering pipelines
  • Shortest-path routing with Dijkstra, A*, and Contraction Hierarchies
  • Real-time traffic ingestion and prediction using ML models
  • Geo-sharding strategies for distributing data globally
  • Complete C# implementation with 300+ lines of production-grade code

2. Functional & Non-Functional Requirements

2.1 Functional Requirements

IDFeatureDescription
F1Map DisplayRender map tiles at multiple zoom levels (0–21), supporting pan, zoom, tilt, and rotation
F2Place SearchSearch for businesses, addresses, landmarks, and coordinates with autocomplete
F3RoutingCompute shortest/fastest routes between two points with multiple transport modes (driving, walking, transit, cycling)
F4Turn-by-Turn NavigationProvide voice-guided, step-by-step navigation with lane guidance
F5Real-Time TrafficDisplay live traffic conditions (green/yellow/red) with incident reports
F6ETA PredictionProvide accurate arrival time estimates based on traffic patterns
F7GeocodingConvert addresses to coordinates and vice versa
F8Street View360-degree panoramic street-level imagery
F9Location SharingShare real-time location with contacts
F10Offline MapsDownload map tiles and routing data for offline use
F11Business ListingsView business details, photos, reviews, and ratings

2.2 Non-Functional Requirements

RequirementTargetRationale
Latency (tile load)< 100ms p99Perceived instant map rendering on scroll/zoom
Latency (routing)< 2s p99Users expect route calculation under 2 seconds
Latency (search)< 200ms p99Autocomplete requires fast prefix search
Availability99.99%Navigation is safety-critical; must be available
Throughput25B+ requests/dayServes 1B+ monthly active users globally
Traffic freshness< 30 secondsReal-time traffic must reflect current conditions
Geo precisionSub-meter accuracyFor turn-by-turn and lane-level navigation
Data durability99.999999999% (11 nines)Map data is extremely expensive to regenerate

3. Capacity Estimation

3.1 Read/Write Traffic

Assumptions: 1B daily active users, average 50 map tile requests per session, 10% of users perform routing, 5% trigger real-time location updates.

OperationCalculationQPS (Peak: 3x avg)
Tile Requests1B x 50 tiles / 86400s = 578K/s~1.7M/s
Place Search1B x 10 searches / 86400s = 116K/s~350K/s
Route Requests1B x 0.10 x 3 / 86400s = 3.5K/s~10K/s
GPS Pings1B x 0.05 x 10 updates / 86400s = 58K/s~175K/s
Geocoding1B x 5 / 86400s = 58K/s~175K/s

3.2 Storage Estimation

Data TypeSize per UnitTotal CountTotal Storage
Map Tiles (vector)~50 KB compressed~2 trillion tiles (all zoom levels)~100 PB (across regions)
Road Network Graph~100 bytes/edge~1 billion road segments~100 GB
Place/POI Data~2 KB/record5 billion places~10 TB
Traffic History~50 bytes/ping10 trillion pings/year~500 TB/year
Street View Images~15 MB/panorama~2 billion panoramas~30 PB
Business Reviews~1 KB/review~20 billion reviews~20 TB

3.3 Bandwidth Estimation

Peak Bandwidth: Serving 1.7M tile requests/second at 50 KB average tile size = 85 GB/s (680 Gbps). This requires extensive CDN caching and geo-distributed tile servers. With 99% CDN cache hit rate, origin servers handle only ~850 MB/s.

4. Data Model — Places, Roads, Segments, Coordinates, Tiles

4.1 Core Entities

erDiagram PLACE { string place_id PK string name string address float latitude float longitude string geohash string category float rating int review_count float popularity_score timestamp last_updated } ROAD_SEGMENT { string segment_id PK string road_name float start_lat float start_lng float end_lat float end_lng float distance_km int speed_limit_kmh int road_type boolean is_bidirectional float congestion_factor } ROAD_NODE { string node_id PK float latitude float longitude int node_type int elevation_meters } MAP_TILE { string tile_id PK int zoom_level int tile_x int tile_y string tile_format long size_bytes string cdn_url string encoding_version } TRAFFIC_SEGMENT { string segment_id FK timestamp recorded_at float current_speed_kmh float free_flow_speed_kmh float congestion_level string traffic_source } USER_LOCATION { string user_id float latitude float longitude float accuracy_meters timestamp recorded_at float speed_kmh float heading } ROAD_NODE ||--o{ ROAD_SEGMENT : connects ROAD_SEGMENT ||--o{ TRAFFIC_SEGMENT : has_traffic PLACE }o--|| ROAD_NODE : nearest_node

4.2 Coordinate System

All coordinates in the system use the WGS 84 (World Geodetic System 1984) datum, which represents latitude and longitude as floating-point numbers. Latitude ranges from -90 to +90 and longitude from -180 to +180. For internal calculations, we convert to E7 format (latitude x 10^7) to use integer arithmetic and avoid floating-point precision issues.

Key Insight: At the equator, 1 degree of latitude is approximately 111 km, and 1 degree of longitude is approximately 111 km. At 45 degrees N latitude, 1 degree of longitude is approximately 78 km. This non-linearity is why naive rectangular grids fail for geospatial indexing and why we need specialized structures like Geohash, QuadTree, or S2 cells.

4.3 Tile Coordinate System

Map tiles follow the Slippy Map tilenames convention. At zoom level z, the world is divided into 2^z x 2^z tiles. Each tile is identified by (x, y, z) coordinates where x is the column (0 to 2^z - 1) and y is the row (0 to 2^z - 1). At zoom level 0, the entire world is one tile (256x256 pixels). At zoom level 21, there are over 2 trillion tiles, each covering approximately 1.1m x 1.1m at the equator.

Zoom LevelTilesResolution (equator)Coverage
01~156 km/pixelEntire world
51,024~4.9 km/pixelCountry-level
101,048,576~153 m/pixelCity-level
15~1 billion~4.8 m/pixelStreet-level
21~4.4 trillion~1.1 m/pixelBuilding-level

5. API Design

5.1 Tile API

GET /tiles/{z}/{x}/{y}.{format}?key={API_KEY}

Returns a map tile image. Format can be png, webp, or pbf (vector tiles). Cache-Control headers set max-age=31536000 since tiles are immutable.

5.2 Place Search API

GET /api/v1/places/search?q={query}&lat={lat}&lng={lng}&radius={meters}&type={category}

Response:

{
    "results": [
        {
            "place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
            "name": "Starbucks Coffee",
            "address": "123 Main St, Seattle, WA 98101",
            "location": { "lat": 47.6062, "lng": -122.3321 },
            "rating": 4.2,
            "reviews": 342,
            "open_now": true,
            "types": ["cafe", "coffee_shop", "food"]
        }
    ],
    "next_page_token": "CpQCBgEAAL..."
}

5.3 Routing API

GET /api/v1/routes?origin={lat},{lng}&destination={lat},{lng}&mode={driving|walking|transit|bicycling}&departure_time={ISO8601}&alternatives={true|false}

Returns route(s) with polyline-encoded overview path, turn-by-turn steps, distance, duration, and ETA considering current traffic conditions.

5.4 Geocoding API

GET /api/v1/geocode?address={address_string}

GET /api/v1/reverse-geocode?lat={lat}&lng={lng}

5.5 Autocomplete API

GET /api/v1/autocomplete?input={partial_text}&lat={lat}&lng={lng}&types={types}

Returns 5 suggestions per keystroke using prefix matching against the Places index. Latency budget: less than 50ms p95.

5.6 Real-Time Traffic API

POST /api/v1/traffic/bounds?sw_lat={lat}&sw_lng={lng}&ne_lat={lat}&ne_lng={lng}&zoom={z}

Returns encoded traffic vectors for the visible bounding box. Supports WebSocket subscription for live updates during navigation.

5.7 Location Update API

POST /api/v1/location/update

Accepts batch GPS coordinates from client devices for traffic data collection. Uses application/protobuf for efficient binary serialization. Rate-limited to 1 update per 5 seconds per user.

6. High-Level Architecture

graph TB subgraph ClientLayer["Client Layer"] MobileApp["Mobile App (iOS/Android)"] WebApp["Web App (JavaScript)"] SDK["Embed SDK"] end subgraph CDNEdge["CDN and Edge"] CDN["CloudFront / Cloud CDN"] EdgeTiles["Edge Tile Cache"] end subgraph APIGateway["API Gateway"] Gateway["API Gateway / Load Balancer"] RateLimit["Rate Limiter"] end subgraph CoreServices["Core Services"] TileService["Tile Rendering Service"] SearchService["Place Search Service"] RouteService["Routing Service"] GeoCodeService["Geocoding Service"] TrafficService["Traffic Service"] NavService["Navigation Service"] end subgraph MLPrediction["ML and Prediction"] TrafficML["Traffic Prediction ML"] ETAModel["ETA Prediction Model"] SearchRank["Search Ranking ML"] end subgraph DataLayer["Data Layer"] TileStore["Tile Storage (GCS/S3)"] GraphDB["Road Graph DB"] PlaceDB["Places DB"] TrafficDB["Traffic Time Series"] CacheLayer["Redis Cluster"] end subgraph DataIngestion["Data Ingestion"] GPSIngest["GPS Ping Ingestion"] Kafka["Kafka / Pub/Sub"] StreamProc["Stream Processing (Flink)"] end MobileApp --> CDN WebApp --> CDN SDK --> CDN CDN --> EdgeTiles MobileApp --> Gateway WebApp --> Gateway Gateway --> RateLimit RateLimit --> TileService RateLimit --> SearchService RateLimit --> RouteService RateLimit --> GeoCodeService RateLimit --> TrafficService TileService --> TileStore TileService --> CDN SearchService --> PlaceDB SearchService --> CacheLayer RouteService --> GraphDB RouteService --> TrafficDB TrafficService --> TrafficDB TrafficML --> TrafficDB ETAModel --> TrafficDB GPSIngest --> Kafka Kafka --> StreamProc StreamProc --> TrafficDB StreamProc --> TrafficService NavService --> RouteService NavService --> TrafficService NavService --> GeoCodeService
Architecture Highlights:
  • CDN-first design: Tiles are immutable and heavily cached at edge locations worldwide, achieving 99%+ cache hit rates
  • Separation of concerns: Routing, search, traffic, and tile rendering are independent microservices that can scale independently
  • Stream processing pipeline: GPS pings flow through Kafka then Flink then Traffic DB for near-real-time traffic updates
  • ML integration: Traffic prediction and ETA models run as separate services with their own GPU clusters

7. Map Tile Rendering — Vector and Raster Tiles

Map tile rendering is the backbone of any mapping system. When a user pans or zooms, the client requests pre-rendered tiles from the nearest CDN edge. The critical insight is that map tiles are effectively immutable — once rendered for a given (x, y, z) coordinate, they never change. This makes them perfect for aggressive caching with far-future expiration headers.

7.1 Raster Tiles

Raster tiles are pre-rendered PNG or WebP images, typically 256x256 or 512x512 pixels. They are generated offline by a tile rendering pipeline that reads road data, place labels, terrain, and satellite imagery, then composites them into flat images. Raster tiles are simple to serve but cannot be dynamically styled — you must re-render the entire tile set for every style variation (day mode, night mode, terrain mode, etc.).

graph LR subgraph RasterPipeline["Raster Tile Pipeline"] A["Road Data + POI Data"] --> B["Tile Renderer"] C["Satellite Imagery"] --> B D["Terrain Elevation"] --> B B --> E["PNG/WebP Tiles (256x256)"] E --> F["GCS/S3 Storage"] F --> G["CDN Edge"] end subgraph VectorPipeline["Vector Tile Pipeline"] H["Raw Map Data"] --> I["Vector Tile Encoder"] I --> J["PBF/MBTiles"] J --> K["Tile Storage"] K --> L["CDN Edge"] L --> M["Client GPU Rendering"] end

7.2 Vector Tiles

Vector tiles are the modern approach used by Google Maps since approximately 2014. Instead of pre-rendered images, they contain structured geometry data (points, lines, polygons) encoded as Protocol Buffer binary. The client-side rendering engine (using WebGL/Metal/Vulkan) composites these geometries into actual pixel output. This enables:

  • Dynamic styling: Day/night mode, terrain, transit overlays — all rendered client-side without new tile downloads
  • Rotation and tilt: 3D building extrusions and perspective views are impossible with raster tiles
  • Smaller payloads: Vector tiles are typically 5-30 KB vs 50-200 KB for raster tiles
  • Interactive features: Click on individual buildings, roads, or polygons because the geometry data is available client-side

7.3 Tile Generation Pipeline

StepProcessTechnologyLatency
1Source data ingestion (OSM, survey, satellite)Apache BeamHours
2Data validation and deduplicationCustom validatorsMinutes
3Graph construction (road network)Custom builderHours
4Vector tile encoding (tippecanoe)tippecanoe / customHours
5Label placement and collision detectionCustom engineHours
6Raster tile rendering (fallback)Mapbox GL / customHours
7Upload to object storage and CDN invalidationGCS + CDN APIMinutes

8. Geospatial Indexing — Geohash, QuadTree, S2

Geospatial indexing is the fundamental problem that makes Google Maps possible. When you search "restaurants near me," the system must efficiently find all restaurants within a radius of your location — across billions of POIs and trillions of possible coordinates. Standard B-tree indexes cannot handle 2D proximity queries efficiently, so we need specialized geospatial data structures.

8.1 Geohash

A Geohash encodes a 2D latitude/longitude pair into a 1D alphanumeric string using a recursive Z-order curve (Morton code). Each additional character adds precision: a 6-character Geohash covers a 1.2 km x 0.6 km area, while an 11-character Geohash provides approximately 1-meter precision. The beautiful property of Geohashes is that nearby points share common prefixes, enabling efficient range queries using standard B-tree indexes.

Geohash Example

LocationLatitudeLongitudeGeohash (6 chars)Precision
Times Square, NYC40.7580-73.9855dr5reg~1.2 km
Empire State Building40.7484-73.9857dr5reu~1.2 km
Central Park40.7829-73.9654dr5rux~1.2 km
Eiffel Tower, Paris48.85842.2945u09tun~1.2 km

Geohash limitations: The Z-order curve creates "edge effects" where nearby points across a Geohash boundary may have very different prefixes. To handle this, production systems query 9 neighboring Geohash cells (the cell + 8 surrounding cells) to ensure no nearby POIs are missed.

8.2 QuadTree

A QuadTree recursively subdivides 2D space into four quadrants until each leaf node contains fewer than a threshold number of points. For maps, the root covers the entire world, and each level splits into NW, NE, SW, SE quadrants. QuadTrees provide O(log n) spatial queries and are excellent for dynamic data, but can become unbalanced in regions with high POI density (like Manhattan).

graph TB Root["World (Level 0)"] --> NW1["NW: Americas + Europe"] Root --> NE1["NE: Asia + Pacific"] Root --> SW1["SW: South America + Antarctica"] Root --> SE1["SE: Australia + Oceania"] NE1 --> NW2["NW: Central Asia"] NE1 --> NE2["NE: East Asia"] NE1 --> SW2["SW: South Asia"] NE1 --> SE2["SE: Southeast Asia"] NW2 --> Leaf1["Leaves: Individual POIs"] NE2 --> Leaf2["Leaves: Individual POIs"] style Leaf1 fill:#10b981,color:#fff style Leaf2 fill:#10b981,color:#fff

8.3 Google S2 Geometry

Google S2 is the gold standard for geospatial indexing in production systems. Unlike Geohash (which uses a flat Z-order curve) or QuadTree (which uses a simple 2D subdivision), S2 projects the Earth's surface onto the faces of a cube and then recursively subdivides each face using a quadratic curve (Hilbert curve). This produces cells with much better spatial locality than Geohash, meaning nearby points always share long S2 cell ID prefixes.

S2 cells are identified by 64-bit unsigned integers, and their level determines the cell size. At level 0, each cell covers one face of the cube (approximately 85 million km2). At level 30, each cell is approximately 1 cm2. Google Maps internally uses S2 level 18 cells (approximately 0.84 m2) as the base tile unit for POI indexing and routing graph nodes.

S2 LevelCell Area (avg)Use Case
085 million km2Top-level cube face
6~2,100 km2Region-level grouping
10~160 km2City-level index
15~0.13 km2Neighborhood-level
18~0.84 m2Building-level POI index
25~0.005 cm2Precision matching
30~0.00005 cm2Maximum precision

Why S2 Wins: The Hilbert curve used by S2 has better spatial locality than the Z-order curve used by Geohash. This means S2 cell IDs for nearby points are almost always numerically close, enabling efficient range scans on sorted indexes. Additionally, S2 cells handle the Earth's curvature naturally by projecting onto a sphere, while Geohash treats latitude/longitude as a flat rectangle.

8.4 Comparison Table

FeatureGeohashQuadTreeGoogle S2
Space Filling CurveZ-order (Morton)Quad subdivisionHilbert curve
Spatial LocalityModerate (edge issues)Good (but unbalanced)Excellent
Sphere HandlingFlat projectionFlat projectionCube projection (native sphere)
Cell ID TypeStringTree pointer64-bit integer
Dynamic UpdatesRe-index neededNatural insert/deleteRe-index needed
Industry UsageRedis, MongoDBGame engines, LODGoogle, Uber H3
Query PerformanceO(n log n) with neighborsO(log n) balancedO(log n) with covers

9. Place Search and POI Database

Google Maps indexes over 5 billion places worldwide — from major landmarks to tiny street food stalls. Place search is not just about proximity; it involves fuzzy text matching, ranking by relevance, personalization, and real-time signals like open/closed status and current wait times.

9.1 Search Architecture

graph LR UserInput["User Types Query"] --> Autocomplete["Autocomplete Service"] Autocomplete --> TrieService["Trie / Prefix Index"] Autocomplete --> FuzzyMatcher["Fuzzy Match + NLP"] UserInput --> SearchEngine["Search Engine"] SearchEngine --> InvertedIndex["Inverted Text Index"] SearchEngine --> GeoFilter["Geo Proximity Filter"] SearchEngine --> Ranker["ML Ranking Model"] Ranker --> Personalization["User History + Context"] Ranker --> Freshness["Open Status + Popularity"] Ranker --> Result["Ranked Results"]

9.2 Autocomplete Pipeline

The autocomplete system must return results within 50ms as the user types each character. This is achieved using a pre-built trie (prefix tree) stored in memory, where each node stores the top 5 ranked completions for that prefix. The trie is sharded by Geohash prefix — each shard handles autocomplete for a geographic region.

When the user types "Star," the system looks up the trie for prefix "star" and returns results like "Starbucks Coffee (0.3 mi)," "Starter Village (1.2 mi)," and "Starlight Diner (0.8 mi)" — ranked by distance from the user's current location combined with the place's popularity score.

9.3 POI Data Schema

{
    "place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
    "name": "Starbucks Coffee",
    "name_languages": { "en": "Starbucks Coffee", "ja": "スターバックス" },
    "address": {
        "street": "123 Main St",
        "city": "Seattle",
        "state": "WA",
        "zip": "98101",
        "country": "US",
        "formatted": "123 Main St, Seattle, WA 98101, USA"
    },
    "location": { "lat": 47.6062, "lng": -122.3321, "s2_cell_id": "928f1a7c" },
    "categories": ["cafe", "coffee_shop", "food_and_drink"],
    "rating": 4.2,
    "review_count": 342,
    "popularity_score": 87.5,
    "business_status": "OPERATIONAL",
    "opening_hours": { "monday": "05:00-22:00", "tuesday": "05:00-22:00" },
    "price_level": 2,
    "photos": ["gs://bucket/photo1.jpg", "gs://bucket/photo2.jpg"],
    "reviews_summary": {
        "sentiment": "positive",
        "top_themes": ["fast service", "good coffee"]
    },
    "current_wait_minutes": 5,
    "crowd_level": "moderate"
}

9.4 Ranking Signals

SignalWeightDescription
Text Relevance35%BM25 score of query vs place name and description
Proximity25%Distance from user to place (exponential decay)
Popularity20%Historical visit frequency and search volume
Rating10%Average user rating with Bayesian smoothing
Freshness5%Open now, recently reviewed, new listing
Personalization5%User's past visit history and preferences

10. Routing Algorithm — Dijkstra, A*, Contraction Hierarchies

Route computation is one of the most computationally intensive aspects of Google Maps. The road network graph contains over 1 billion nodes (intersections) and 2 billion edges (road segments). Computing the shortest path across this graph in real-time — while considering traffic, tolls, U-turns, and turn restrictions — requires sophisticated algorithms far beyond basic Dijkstra.

10.1 Dijkstra's Algorithm

Dijkstra's algorithm guarantees the shortest path but explores all directions equally, making it O((V + E) log V) where V is nodes and E is edges. On a billion-node graph, this takes minutes — far too slow for real-time use. However, it serves as the theoretical foundation for all other routing algorithms.

10.2 A* Algorithm

A* improves Dijkstra by using a heuristic function to guide the search toward the destination. The heuristic is typically the straight-line (Euclidean or Haversine) distance from the current node to the destination, which is admissible (never overestimates) and consistent. A* explores significantly fewer nodes — typically 5-10% of Dijkstra's node expansion — for point-to-point queries.

graph TB subgraph DijkstraExploration["Dijkstra Exploration"] D1["Start"] --> D2["Explore ALL neighbors"] D2 --> D3["And THEIR neighbors"] D3 --> D4["And THEIR neighbors..."] D4 --> D5["Eventually reaches Goal"] end subgraph AStarExploration["A* Exploration"] A1["Start"] --> A2["Explore neighbors closer to Goal"] A2 --> A3["Focus in Goal direction"] A3 --> A4["Reaches Goal quickly"] end style D5 fill:#ef4444,color:#fff style A4 fill:#10b981,color:#fff

10.3 Contraction Hierarchies (CH)

Contraction Hierarchies is the breakthrough algorithm that enables millisecond route computation on continental-scale road networks. The key insight is to pre-process the graph by identifying "important" nodes (highway intersections, major junctions) and creating shortcut edges that skip over unimportant intermediate nodes.

The preprocessing phase (run offline, taking hours) contracts nodes in order of importance. When a node is contracted, shortcut edges are added between its neighbors that represent the shortest path through that node. After preprocessing, the query phase runs a bidirectional Dijkstra that only explores nodes in increasing order of importance — forward from source and backward from destination — meeting in the middle at a high-importance node. This reduces query time from seconds to microseconds.

AlgorithmPreprocessingQuery TimeMemoryOptimal?
DijkstraNoneO((V+E) log V)O(V+E)Yes (single-source)
A*NoneO(E) worst caseO(V+E)Yes (with admissible h)
Contraction HierarchiesHours (offline)O(V log V) amortizedO(V log V)Yes
ALT (A* + Landmarks)MinutesO(V log V)O(V x K)Yes
Transit Node RoutingHoursO(1) for long distanceO(V2)Yes (long queries)

10.4 Multi-Modal Routing

Google Maps supports driving, walking, cycling, and transit routing. Each mode uses a different graph layer:

  • Driving: Road network with one-way restrictions, turn penalties, speed limits, and traffic weights
  • Walking: Pedestrian paths, sidewalks, crosswalks, and pedestrian-only zones
  • Cycling: Bike lanes, bike-friendly roads, elevation penalties, and bike-sharing stations
  • Transit: Time-dependent graph where edges represent bus/train routes with schedules, transfer times, and walking connections
Interview Tip: When asked about routing, always mention that naive Dijkstra is too slow for production use. Discuss Contraction Hierarchies as the standard approach, and mention that real systems combine CH with time-dependent edge weights for traffic-aware routing. For transit routing, emphasize that the graph changes based on time-of-day schedules.

11. Real-Time Traffic Data Collection

Google Maps' real-time traffic feature is powered by an anonymous crowdsourcing engine that processes GPS data from over 1 billion Android devices and millions of vehicles worldwide. Every few seconds, participating devices report their anonymized location, speed, and heading to Google's servers, creating the world's largest real-time traffic sensor network.

11.1 Data Collection Pipeline

graph LR subgraph DataSources["Data Sources"] Android["Android Devices"] Taxis["Fleet Vehicles / Taxis"] Sensors["Road Sensors"] Government["Government Data"] Waze["Waze Reports"] end subgraph Ingestion["Ingestion Layer"] LB["Load Balancer"] Validator["Data Validation"] Dedup["Deduplication"] end subgraph Processing["Processing Pipeline"] Kafka["Kafka (Partitioned by Geo)"] Flink["Apache Flink"] MapMatch["Map Matching"] SpeedCalc["Speed Aggregation"] Anomaly["Anomaly Detection"] end subgraph Storage["Storage"] TSDB["Time-Series DB"] GraphUpdate["Graph Edge Weights"] TrafficTiles["Traffic Overlay Tiles"] end Android --> LB Taxis --> LB Sensors --> LB Government --> LB Waze --> LB LB --> Validator Validator --> Dedup Dedup --> Kafka Kafka --> Flink Flink --> MapMatch MapMatch --> SpeedCalc SpeedCalc --> Anomaly Anomaly --> TSDB SpeedCalc --> GraphUpdate SpeedCalc --> TrafficTiles

11.2 Map Matching

Raw GPS coordinates from devices are imprecise — typical smartphone GPS has 5-15 meter accuracy, and urban canyons can cause 50+ meter errors. Map matching is the process of snapping raw GPS points to the most likely road segment. This uses the Hidden Markov Model (HMM) approach where:

  • Hidden states = road segments
  • Observations = raw GPS points
  • Emission probability = likelihood of GPS observation given the vehicle is on a particular road segment (Gaussian distribution based on distance)
  • Transition probability = likelihood of moving from one road segment to the next (based on road network connectivity and distance traveled)

The Viterbi algorithm is then used to find the most likely sequence of road segments that the vehicle traveled on.

11.3 Speed Aggregation

After map-matching, individual vehicle speeds are aggregated per road segment. The system computes:

MetricCalculationPurpose
Average SpeedWeighted mean of all vehicles on segmentBasic traffic flow indicator
Free-Flow Speed95th percentile speed over 6 monthsBaseline for congestion calculation
Congestion Ratiocurrent_speed / free_flow_speed0.0 = gridlock, 1.0 = free flow
Flow RateVehicles per hour per laneCapacity utilization
Incident ScoreSudden speed drop + anomaly detectionAccident/construction detection

12. Traffic Prediction and ETA

Predicting future traffic conditions is arguably the most valuable and technically challenging aspect of Google Maps. The ETA prediction system combines historical patterns, real-time conditions, and machine learning models to predict travel time with remarkable accuracy.

12.1 Prediction Pipeline

graph TB subgraph Features["Feature Engineering"] F1["Historical Speed Profiles"] F2["Current Real-Time Speeds"] F3["Time of Day / Day of Week"] F4["Weather Conditions"] F5["Special Events"] F6["Road Construction"] F7["User Route Patterns"] end subgraph Models["ML Models"] LSTM["LSTM / Temporal Fusion Transformer"] XGB["XGBoost (Per-Segment)"] GraphNN["Graph Neural Network"] end subgraph Outputs["Prediction Output"] SegmentETA["Per-Segment Speed Prediction"] RouteETA["End-to-End Route ETA"] Confidence["Confidence Intervals"] end F1 --> LSTM F2 --> LSTM F3 --> LSTM F4 --> XGB F5 --> XGB F6 --> GraphNN F7 --> GraphNN LSTM --> SegmentETA XGB --> SegmentETA GraphNN --> RouteETA SegmentETA --> RouteETA RouteETA --> Confidence

12.2 ETA Computation

ETA prediction works in two phases:

Phase 1 — Per-Segment Prediction: For each road segment on the route, predict the speed at the time the vehicle will reach that segment. If the route is 30 minutes long and the current time is 2:00 PM, the system needs to predict speeds at 2:00 PM for the first segments, 2:10 PM for middle segments, and 2:30 PM for the final segments. This is a time-dependent routing problem.

Phase 2 — Route-Level Aggregation: Sum the predicted travel times across all segments, accounting for turn delays at intersections (typically 10-30 seconds per turn), traffic light wait times, and merge bottlenecks.

ETA Accuracy Metrics:
  • Median absolute percentage error: approximately 3% for short trips (less than 5 miles)
  • Median absolute percentage error: approximately 6% for long trips (greater than 50 miles)
  • Improvement over pure historical averages: approximately 50% error reduction
  • Model retraining frequency: Daily for XGBoost, Weekly for deep learning models

12.3 Event Impact Modeling

Special events (concerts, sports games, conferences) create anomalous traffic patterns that historical data cannot predict. Google Maps integrates event data from multiple sources (venue schedules, sports calendars, community reports) and trains a separate event impact model that estimates the spatial and temporal extent of traffic disruption caused by each event. This model considers event capacity, start/end times, attendee origin distribution, and available transit alternatives.

14. Offline Maps and Tile Caching

Offline maps allow users to navigate without an internet connection — critical for areas with poor connectivity, international travel without data plans, and battery conservation. Google Maps lets users download entire regions (containing vector tiles, road data, and POI information) for offline use.

14.1 Offline Data Package

ComponentSize (typical city)Content
Vector Tiles50-200 MBMap geometry for all zoom levels 0-14
Road Graph10-50 MBRouting data for driving and walking
POI Index5-20 MBPlace names, addresses, categories, coordinates
Traffic Patterns2-5 MBHistorical speed profiles (no real-time)
Geocoding Index1-3 MBAddress-to-coordinate mapping
Total70-280 MB

14.2 Tile Caching Strategy

For online use, the client maintains a multi-tier cache for map tiles:

  • L1 Cache (Memory): Most recently viewed approximately 200 tiles (approximately 10 MB). Eviction: LRU. Access time: less than 1ms
  • L2 Cache (Disk): Approximately 5,000 tiles (approximately 250 MB) covering the user's frequently visited areas. Eviction: LRU with geographic clustering. Access time: approximately 5ms
  • L3 Cache (CDN Edge): Regional tile cache served by CDN POPs. Access time: approximately 20ms
  • Origin: Tile rendering servers + cloud storage. Access time: approximately 200ms
Prefetching Strategy: When a user starts navigation, the client prefetches all tiles along the route at zoom levels 12-16, plus a buffer zone of 2km on each side. For a 30-minute drive, this typically downloads approximately 500 tiles (25 MB) in the background, ensuring smooth tile display even when cellular connectivity is intermittent.

15. Street View and Imagery

Street View provides 360-degree panoramic imagery of streets worldwide, covering over 16 million kilometers of roads in 100+ countries. Each Street View panorama is a collection of photos stitched together into an equirectangular projection that users can explore interactively.

15.1 Street View Data Pipeline

graph LR Capture["Car/Backpack/Trekker Capture"] --> Upload["Image Upload (GPS-tagged)"] Upload --> Stitch["Panorama Stitching"] Stitch --> Blur["Face and Plate Blurring (ML)"] Blur --> GeoTag["Precise Geo-Tagging"] GeoTag --> Process["HDR + Color Balance"] Process --> Store["Panorama Storage (PB-scale)"] Store --> Serve["Tile-based Serving (zoom levels)"]

15.2 Storage Requirements

MetricValue
Total panoramas~2 billion
Size per panorama (all zoom levels)~15 MB (compressed)
Total storage~30 PB (raw + processed)
Capture cars worldwide~1,000 vehicles
Photos per panorama~15 (multi-camera rig)
Resolution per photo~50 megapixels

16. Business Listings and Reviews

Google Maps' business listings and reviews ecosystem is the world's largest local business directory, with over 200 million businesses listed and 20+ billion reviews. This data transforms Google Maps from a pure navigation tool into a local discovery platform.

16.1 Business Data Model

CREATE TABLE business_listings (
    business_id     BIGINT PRIMARY KEY,
    place_id        VARCHAR(64) UNIQUE NOT NULL,
    name            VARCHAR(256) NOT NULL,
    category_primary VARCHAR(128),
    categories      JSON,
    address         JSON,
    location        GEOGRAPHY(POINT, 4326),
    s2_cell_id      BIGINT,
    phone           VARCHAR(32),
    website_url     VARCHAR(512),
    rating_avg      DECIMAL(3,2),
    rating_count    INT,
    price_level     SMALLINT,
    hours           JSON,
    attributes      JSON,
    photos          JSON,
    status          VARCHAR(32),
    verified        BOOLEAN,
    created_at      TIMESTAMP,
    updated_at      TIMESTAMP
);

CREATE INDEX idx_business_location
    ON business_listings USING GIST(location);
CREATE INDEX idx_business_s2
    ON business_listings(s2_cell_id);
CREATE INDEX idx_business_category
    ON business_listings(category_primary);
CREATE INDEX idx_business_rating
    ON business_listings(rating_avg DESC, rating_count DESC);

16.2 Review Sentiment Analysis

Google uses NLP models to analyze review text and extract:

  • Sentiment score: Positive/negative/neutral classification per review
  • Topic extraction: Key themes like "fast service," "long wait," "great food," "dirty restrooms"
  • Photo relevance: Matching uploaded photos to review topics
  • Fake review detection: ML model to identify and filter fake/spam reviews with greater than 95% precision

17. Geocoding and Reverse Geocoding

Geocoding converts a human-readable address ("1600 Amphitheatre Parkway, Mountain View, CA") into geographic coordinates (37.4220, -122.0841). Reverse geocoding does the opposite — converting coordinates to a readable address. These are foundational services used by millions of API calls daily.

17.1 Geocoding Pipeline

graph TB Input["Address String: 1600 Amphitheatre Pkwy, Mountain View"] Input --> Tokenizer["Tokenizer + Normalizer"] Tokenizer --> Parser["Address Parser (NER)"] Parser --> Structured["Structured Address"] Structured --> Lookup["Prefix Lookup (Elasticsearch)"] Lookup --> Candidates["Top 10 Candidates"] Candidates --> Ranker["Spatial Ranking"] Ranker --> Result["Best Match: 37.4220, -122.0841"] subgraph ReverseGeocoding["Reverse Geocoding"] LatLng["Input: 37.4220, -122.0841"] LatLng --> S2Lookup["S2 Cell Lookup"] S2Lookup --> RoadMatch["Nearest Road Match"] RoadMatch --> HNInterp["House Number Interpolation"] HNInterp --> AddrResult["1600 Amphitheatre Parkway, Mountain View, CA"] end

17.2 Address Parsing with NER

Address parsing is treated as a Named Entity Recognition (NER) problem where tokens are classified as house_number, street, city, state, zip_code, country. Google's geocoder handles massive variation in address formats across 200+ countries — from US addresses (house number, street, city, state, zip) to Japanese addresses (prefecture, city, district, block, building) which are ordered from large to small.

Edge Cases: The geocoder must handle ambiguous addresses ("Main Street" exists in nearly every US city), misspellings ("Amphitheatre" vs "Amphitheater"), missing components (no zip code), and multiple interpretations (intersection addresses "Main St & 1st Ave"). The system returns multiple candidates ranked by confidence, and the client or calling application can disambiguate using user context.

18. Location Sharing and Live View

Location sharing allows users to broadcast their real-time position to selected contacts, enabling coordination and safety. The system must handle millions of concurrent location streams, with each stream pushing updates every 1-3 seconds to contacts viewing the shared location.

18.1 Architecture

ComponentTechnologyResponsibility
Location IngestionKafka + FlinkReceive GPS pings from sharing users
Presence ServiceRedis + Pub/SubTrack who is sharing with whom
Push DeliveryWebSocket / FCM / APNsPush location updates to viewers
Location StorageTime-series DBStore location history for the sharing session
Access ControlRBAC + ACLEnsure only authorized contacts can view

18.2 Live View (AR Navigation)

Google Maps' Live View uses the phone's camera and ARCore/ARKit to overlay navigation arrows on the real-world camera view. This requires:

  • Precise localization: Matching camera frames to Street View imagery using visual positioning system (VPS) — achieving sub-meter accuracy
  • 3D anchor placement: Rendering navigation arrows that appear anchored to real-world positions
  • IMU fusion: Combining camera data with accelerometer and gyroscope for smooth tracking
  • Minimal latency: AR rendering must maintain 60fps with less than 20ms tracking latency to avoid motion sickness

19. Database Sharding — Geo-Sharding

Storing all global map data in a single database is impossible. Google Maps uses geo-sharding — partitioning data by geographic regions — so that queries for a specific area are served by a single shard, minimizing cross-shard communication.

19.1 Sharding Strategy

graph TB subgraph GlobalGrid["Global Geohash Grid"] A["Geohash Prefix 9"] --> B["9q - North America"] A --> C["9r - North Atlantic"] A --> D["9x - Europe"] A --> E["9y - Russia/Central Asia"] A --> F["9z - South Asia"] end subgraph Shards["Shard Distribution"] B --> B1["Shard: US-West (Oregon)"] B --> B2["Shard: US-East (Virginia)"] B --> B3["Shard: Canada (Montreal)"] D --> D1["Shard: EU-West (Dublin)"] D --> D2["Shard: EU-Central (Frankfurt)"] F --> F1["Shard: India (Mumbai)"] F --> F2["Shard: SEA (Singapore)"] end subgraph ShardContent["Each Shard Contains"] S1["Road Graph Segment"] S2["POI Data"] S3["Tile Metadata"] S4["Traffic History"] end

19.2 Shard Key Design

The shard key is derived from the S2 cell ID at level 6 (approximately 2,100 km2 per cell). Each S2 level-6 cell maps to a primary database shard. When a query spans multiple cells (e.g., a route from NYC to LA), the routing service queries shards sequentially along the route path and merges results.

Shard RegionS2 CellsData VolumeDB Cluster
North America~45 level-6 cells~5 TB3 replicas x 8 nodes
Europe~40 level-6 cells~4 TB3 replicas x 8 nodes
Asia-Pacific~60 level-6 cells~6 TB3 replicas x 8 nodes
South America~25 level-6 cells~2 TB3 replicas x 4 nodes
Africa~20 level-6 cells~1.5 TB3 replicas x 4 nodes

20. Caching Strategy

Google Maps employs a multi-layered caching strategy across the entire stack. Given that the same tile data is requested millions of times per second, effective caching is the difference between a responsive map and a system collapse.

20.1 Cache Layers

LayerTechnologySizeTTLHit Rate
Browser CacheService Worker + IndexedDB50-200 MB7 days (tiles)60%
Mobile App CacheSQLite + File System500 MB - 2 GB30 days75%
CDN EdgeCloudFront / Cloud CDNPB-scale (distributed)1 year (immutable tiles)95%
Application CacheRedis Cluster100 GB per region5 min (search), 1 hr (POI)85%
Database CacheMySQL query cache + SSD buffer512 GB per shardPersistent (warm)90%

20.2 Cache Invalidation Strategies

  • Map Tiles (immutable): Tiles are content-addressed — the URL encodes the exact version. Old tiles expire naturally. New tile versions get new URLs. No invalidation needed.
  • POI Data (semi-static): TTL of 1 hour. Write-through invalidation when POI is updated. Version stamping in Redis allows stale reads during invalidation storms.
  • Traffic Data (dynamic): TTL of 30 seconds. No caching at application layer — always read from traffic service. Client-side display only caches for smooth animation.
  • Search Results (personalized): TTL of 5 minutes. Per-user cache keys. Invalidated on user location change greater than 500 meters.

21. Multi-Region Design

Google Maps operates across 30+ data center regions worldwide, with each region serving nearby users with minimal latency. The multi-region architecture must handle data replication, consistency trade-offs, and failover while maintaining sub-100ms tile delivery globally.

graph TB subgraph RegionUSW["Region: US-West (Oregon)"] USW_Tile["Tile Servers"] USW_Route["Routing Servers"] USW_Traffic["Traffic Processing"] USW_DB["Primary DB (Americas)"] end subgraph RegionEUW["Region: EU-West (Dublin)"] EUW_Tile["Tile Servers"] EUW_Route["Routing Servers"] EUW_Traffic["Traffic Processing"] EUW_DB["Primary DB (Europe)"] end subgraph RegionAPAC["Region: APAC (Singapore)"] APC_Tile["Tile Servers"] APC_Route["Routing Servers"] APC_Traffic["Traffic Processing"] APC_DB["Primary DB (Asia)"] end USW_DB -->|Async Replication| EUW_DB EUW_DB -->|Async Replication| APC_DB APC_DB -->|Async Replication| USW_DB USW_Traffic -->|Traffic Sync| EUW_Traffic EUW_Traffic -->|Traffic Sync| APC_Traffic

21.1 Data Replication

Data TypeReplication ModeConsistencyLatency Impact
Map TilesMulti-region active-activeEventual (versions)None (local read)
Road GraphMulti-region active-activeEventual (daily sync)None (local read)
POI DataMulti-region active-passiveEventual (hourly sync)None (local read)
Traffic DataActive-active with mergeStrong within regionNone (local processing)
User DataSingle-region primaryStrong~100ms cross-region
Business ReviewsMulti-region active-activeEventual (minutes)None (local read)

21.2 Failover Strategy

If a region fails, traffic is rerouted to the nearest healthy region via DNS failover (Google Cloud DNS with health checks). The failover process takes 30-60 seconds due to DNS TTL propagation. During failover, users may experience slightly higher latency (50-100ms additional) but maintain full functionality.

22. Cost Estimation

22.1 Infrastructure Costs (Annual)

ComponentConfigurationMonthly CostAnnual Cost
Tile CDN20 PB transfer/month at $0.02/GB$400,000$4.8M
Tile Storage (GCS/S3)100 PB at $0.02/GB/month$2,000,000$24M
Tile Rendering Servers500 GPU instances (A100)$1,500,000$18M
Routing Servers2,000 compute instances$800,000$9.6M
Search Servers1,000 instances + Elasticsearch$500,000$6M
Traffic Processing500 Flink instances + Kafka$300,000$3.6M
Database Clusters50 shards x 3 replicas$600,000$7.2M
Redis Cache10 TB cluster (global)$200,000$2.4M
ML Training (GPU)100 TPU v4 pods$500,000$6M
NetworkingCross-region + internet$300,000$3.6M
Street View Storage30 PB at $0.02/GB/month$600,000$7.2M
GPS Ingestion1M QPS sustained$100,000$1.2M
Total Infrastructure Cost$7.8M$93.6M

Note: These are rough estimates for a hypothetical Google Maps-scale system. Google's actual costs are likely different due to their custom hardware (TPUs, custom servers), economies of scale, and vertical integration. The costs would be significantly lower for a startup-scale implementation serving 1M daily users (roughly $50K-$200K/month).

22.2 Revenue Streams

SourceRevenue ModelEstimated Annual Revenue
Google Maps Platform APIsPay-per-use ($2-$35 per 1K requests)$2B+
Local Ads (Promoted Pins)Cost-per-click ($1-$5 CPC)$5B+
Business Profile (premium features)Monthly subscription$500M+
Licensing (data to third parties)Annual licensing fees$1B+

23. Interview Q&A — 12 Questions

Q1: How would you design a system that handles 25 billion map tile requests per day?

Answer: Start with a CDN-first architecture. Map tiles are immutable (versioned by zoom/x/y/hash), so they cache perfectly at edge nodes worldwide with 99%+ hit rates. The origin server handles only 1% of traffic (approximately 250M requests/day = approximately 2,900 QPS). Use WebP/AVIF for 30-50% smaller images vs PNG. Implement a multi-tier cache: browser then mobile app then CDN edge then origin. For vector tiles, payloads are 5-30KB vs 50-200KB for raster, further reducing bandwidth. Pre-render tiles offline in a batch pipeline and store in GCS/S3, which serves as the origin for CDN.

Q2: How would you design the real-time traffic system?

Answer: Collect anonymous GPS pings from millions of devices via a Kafka ingestion pipeline. Use Apache Flink for stream processing: map-match GPS points to road segments using Hidden Markov Models, then aggregate speeds per segment per time window (5-minute buckets). Store results in a time-series database for historical analysis. For real-time display, update a Redis cache with current congestion levels per segment and generate pre-rendered traffic overlay tiles every 30 seconds. For ETA prediction, use a Temporal Fusion Transformer model trained on historical speed profiles, real-time conditions, weather, and event data.

Q3: How would you implement turn-by-turn navigation at scale?

Answer: Pre-compute the route using Contraction Hierarchies on the road graph, storing the sequence of road segments. On the device, continuously GPS-track the user's position and map-match to the nearest road segment. When the matched segment changes, update the position along the pre-computed route. Generate navigation instructions based on upcoming maneuver type (turn, exit, merge) and distance. For rerouting, detect when the user deviates more than 50m from the route and trigger a new route computation. Voice guidance uses pre-recorded snippets for common instructions combined with neural TTS for street names.

Q4: Explain the difference between Geohash and S2 cells. When would you use each?

Answer: Geohash uses a Z-order (Morton) curve to flatten 2D coordinates into a 1D string, making it queryable with standard B-tree indexes. It is simple to implement and works well for basic proximity queries, but has edge effects (nearby points across Geohash boundaries have different prefixes). Google S2 uses a Hilbert curve projected onto a sphere via cube faces, providing superior spatial locality and natural handling of Earth's curvature. S2 is preferred for production systems (used by Google, Uber H3). Use Geohash for simpler applications or when using databases with native Geohash support (Redis, MongoDB). Use S2 for high-performance spatial queries at scale.

Q5: How would you handle offline maps?

Answer: For offline mode, download a pre-packaged data bundle containing: (1) vector tiles up to zoom level 14 for the selected region, (2) a compact road graph for routing, (3) a compressed POI index for search, and (4) historical traffic speed profiles for ETA estimation. The package for a typical city is 100-300 MB. Store in SQLite on the device with a custom tile provider that intercepts tile requests and serves from local storage. For offline routing, use a simplified Contraction Hierarchies implementation with the downloaded road graph. ETA accuracy degrades without real-time traffic, but historical patterns provide reasonable estimates.

Q6: How do you shard a road graph across multiple servers for routing?

Answer: Use geographic partitioning based on S2 cells or administrative boundaries. Each shard contains the road graph nodes and edges within its region. For routes within a single shard, query that shard directly. For cross-shard routes (e.g., NYC to LA), use a hierarchical approach: first find the entry/exit points at shard boundaries, then compute intra-shard routes for each segment. Contraction Hierarchies naturally support this via the hierarchical node ordering — high-importance nodes (highways) span multiple shards. An alternative is to use a "hub-and-spoke" model where each shard has a complete summary of highway connections to neighboring shards.

Q7: How would you design the autocomplete feature for place search?

Answer: Build an in-memory trie data structure where each node stores the top 5 ranked completions for that prefix. Shard the trie by geographic region (using S2 level-6 cells). When the user types, query the local region's trie shard with the prefix and merge results from nearby shards if the user is near a region boundary. The trie is pre-computed from the POI database and rebuilt daily. For ranking within autocomplete, combine text match score, distance from user, and popularity. The entire trie for a city fits in 1-2 GB of memory, and lookup latency is less than 10ms.

Q8: How would you handle map data updates (new roads, closed businesses)?

Answer: Use a versioned tile system. Map data changes trigger a tile regeneration pipeline: affected tiles are re-rendered with new version numbers and uploaded to object storage. The CDN picks up new tiles via content-addressed URLs. The road graph is updated via a graph diffing system: new segments are added, closed roads are marked as inactive, and routing weights are recalculated. These updates flow through a data pipeline that takes hours to propagate globally. For real-time road closures (accidents, construction), use a separate incident overlay system that modifies routing weights in real-time without regenerating tiles.

Q9: How would you estimate the cost of serving Google Maps?

Answer: The biggest cost drivers are: (1) CDN bandwidth — 20+ PB/month at $0.02/GB = approximately $400K/month, (2) Storage — 100+ PB of tile and imagery data = approximately $2.5M/month, (3) Compute — thousands of routing, search, and traffic processing servers = approximately $3M/month, (4) ML infrastructure — GPU clusters for traffic prediction = approximately $500K/month, (5) Database — sharded clusters across regions = approximately $600K/month. Total infrastructure cost is roughly $7-10M/month ($80-120M/year). Revenue from API licensing, local ads, and business listings exceeds $8B annually, making it extremely profitable.

Q10: How would you design the Street View system?

Answer: Capture 360-degree panoramas using multi-camera rigs mounted on vehicles, backpacks, or trekker equipment. Each rig captures approximately 15 synchronized photos at 50MP each. Upload raw images to cloud storage, then run a processing pipeline: GPS-tag each panorama, stitch images into equirectangular projections, apply HDR processing, blur faces and license plates using ML detection, and generate multi-resolution tiles (like map tiles). Store processed panoramas as tiled datasets (5-15MB each). Serve via a tile-based system similar to map tiles — the client requests tiles based on zoom level and viewing angle. Use Street View images as training data for the Visual Positioning System (VPS) that enables AR navigation.

Q11: How do you handle map matching when GPS accuracy is poor (urban canyons, tunnels)?

Answer: In urban areas with tall buildings, GPS accuracy degrades to 50+ meters, and in tunnels, GPS is completely unavailable. To handle this, use a multi-sensor fusion approach: combine GPS with IMU (inertial measurement unit) dead reckoning, barometric altitude, Wi-Fi positioning, Bluetooth beacon detection, and visual landmarks. The map matching algorithm (HMM-based) handles uncertainty by maintaining probabilities over multiple candidate road segments. In tunnels, dead reckoning provides an estimate, and the system snaps to the known tunnel path. Google Maps also uses "GPS shadows" — a database of known GPS-degraded areas — to apply specialized matching algorithms.

Q12: How would you design the ETA prediction system to achieve 95%+ accuracy?

Answer: Combine multiple data sources and models: (1) Historical speed profiles per road segment per time-of-day/day-of-week, (2) Real-time speeds from GPS crowdsourcing, (3) Weather data (rain reduces speeds by 10-30%), (4) Event calendars (sports games, concerts create localized congestion), (5) Construction schedules and road closures. Use a Temporal Fusion Transformer for per-segment speed prediction that handles all these inputs. For route-level ETA, aggregate segment predictions along the route considering the time-dependent nature — the vehicle reaches each segment at a different time. Maintain confidence intervals (p10, p50, p90) rather than point estimates. Continuously evaluate against actual travel times and retrain models weekly.

24. Full C# Implementation — Maps and Navigation System

The following is a production-grade C# implementation covering the core components of a maps and navigation system: geospatial indexing with S2-like cells, a road graph with routing, tile management, traffic aggregation, and a navigation engine. The complete implementation exceeds 300 lines and demonstrates how these components work together.

using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;

namespace MapsNavigationSystem
{
    #region Geospatial Primitives

    public readonly struct GeoPoint : IEquatable<GeoPoint>
    {
        public double Latitude { get; }
        public double Longitude { get; }

        public GeoPoint(double latitude, double longitude)
        {
            Latitude = latitude;
            Longitude = longitude;
        }

        public bool Equals(GeoPoint other) =>
            Latitude.Equals(other.Latitude) &&
            Longitude.Equals(other.Longitude);

        public override bool Equals(object? obj) =>
            obj is GeoPoint p && Equals(p);

        public override int GetHashCode() =>
            HashCode.Combine(Latitude, Longitude);

        public override string ToString() =>
            $"({Latitude:F6}, {Longitude:F6})";

        public static double HaversineDistance(
            GeoPoint a, GeoPoint b)
        {
            const double R = 6371000; // Earth radius in meters
            double dLat = ToRadians(b.Latitude - a.Latitude);
            double dLon = ToRadians(b.Longitude - a.Longitude);
            double lat1 = ToRadians(a.Latitude);
            double lat2 = ToRadians(b.Latitude);

            double h = Math.Sin(dLat / 2) *
                       Math.Sin(dLat / 2) +
                       Math.Cos(lat1) * Math.Cos(lat2) *
                       Math.Sin(dLon / 2) *
                       Math.Sin(dLon / 2);
            return R * 2 * Math.Atan2(
                Math.Sqrt(h), Math.Sqrt(1 - h));
        }

        private static double ToRadians(
            double degrees) => degrees * Math.PI / 180.0;
    }

    public readonly struct BoundingBox
    {
        public GeoPoint SouthWest { get; }
        public GeoPoint NorthEast { get; }

        public BoundingBox(GeoPoint sw, GeoPoint ne)
        {
            SouthWest = sw;
            NorthEast = ne;
        }

        public bool Contains(GeoPoint point) =>
            point.Latitude >= SouthWest.Latitude &&
            point.Latitude <= NorthEast.Latitude &&
            point.Longitude >= SouthWest.Longitude &&
            point.Longitude <= NorthEast.Longitude;
    }

    #endregion
#region Geospatial Index - S2-like Cell System public class GeoCellIndex { private readonly int _level; private readonly Dictionary<long, List<GeoPoint>> _cells; public GeoCellIndex(int level = 18) { _level = level; _cells = new Dictionary<long, List<GeoPoint>>(); } public long GetCellId(GeoPoint point) { double latRad = point.Latitude * Math.PI / 180.0; double lngRad = point.Longitude * Math.PI / 180.0; int face = GetFace(latRad, lngRad); double u = Math.Tan(lngRad); double v = Math.Tan(latRad) / Math.Cos(lngRad); long cellId = face << (_level * 2); long i = 0; double s = Math.PI / 2; for (int k = 1; k <= _level; k++) { int section = 0; double midLat = latRad + (v >= 0 ? s / 2 : -s / 2); double midLng = lngRad + (u >= 0 ? s / 2 : -s / 2); if (Math.Abs(latRad) > Math.Abs(midLat)) section |= 2; if (Math.Abs(lngRad) > Math.Abs(midLng)) section |= 1; i = (i << 2) | section; s /= 2; } cellId |= i; return cellId; } public List<GeoPoint> Query( GeoPoint center, double radiusMeters) { var results = new List<GeoPoint>(); long centerCell = GetCellId(center); long cellRange = (long)(radiusMeters / GetCellSizeMeters()) + 1; for (long dx = -cellRange; dx <= cellRange; dx++) { long neighborCell = centerCell + dx; if (_cells.TryGetValue( neighborCell, out var points)) { foreach (var point in points) { if (GeoPoint.HaversineDistance( center, point) <= radiusMeters) results.Add(point); } } } return results; } public void Insert(GeoPoint point) { long cellId = GetCellId(point); if (!_cells.ContainsKey(cellId)) _cells[cellId] = new List<GeoPoint>(); _cells[cellId].Add(point); } public int Count => _cells.Values.Sum(l => l.Count); private int GetFace( double latRad, double lngRad) => (int)((latRad + Math.PI / 2) / (Math.PI / 3)); private double GetCellSizeMeters() => 40075000.0 / Math.Pow(2, _level) * Math.Cos(0); } #endregion #region Road Graph and Routing public enum RoadType { Highway = 0, MajorRoad = 1, MinorRoad = 2, Residential = 3, Pedestrian = 4, BikePath = 5 } public class RoadNode { public string Id { get; set; } = string.Empty; public GeoPoint Location { get; set; } public int Importance { get; set; } public List<RoadEdge> Edges { get; set; } = new(); public RoadNode(string id, GeoPoint location, int importance = 0) { Id = id; Location = location; Importance = importance; } } public class RoadEdge { public string Id { get; set; } = string.Empty; public string FromNodeId { get; set; } = string.Empty; public string ToNodeId { get; set; } = string.Empty; public string RoadName { get; set; } = string.Empty; public double DistanceMeters { get; set; } public double SpeedLimitKmh { get; set; } public RoadType RoadType { get; set; } public bool IsOneWay { get; set; } public double CurrentSpeedKmh { get; set; } public List<GeoPoint> Polyline { get; set; } = new(); public double FreeFlowTimeSeconds => DistanceMeters / (SpeedLimitKmh / 3.6); public double CurrentTimeSeconds => CurrentSpeedKmh > 0 ? DistanceMeters / (CurrentSpeedKmh / 3.6) : FreeFlowTimeSeconds * 2.0; public double CongestionLevel => CurrentSpeedKmh > 0 ? 1.0 - (CurrentSpeedKmh / SpeedLimitKmh) : 1.0; } public class RoadGraph { private readonly Dictionary<string, RoadNode> _nodes = new(); private readonly Dictionary<string, RoadEdge> _edges = new(); private readonly GeoCellIndex _nodeIndex; public RoadGraph() { _nodeIndex = new GeoCellIndex(level: 15); } public void AddNode(RoadNode node) { _nodes[node.Id] = node; _nodeIndex.Insert(node.Location); } public void AddEdge(RoadEdge edge) { _edges[edge.Id] = edge; if (_nodes.TryGetValue( edge.FromNodeId, out var fromNode)) fromNode.Edges.Add(edge); } public RoadNode? FindNearestNode( GeoPoint point) { var candidates = _nodeIndex.Query(point, 500); RoadNode? nearest = null; double minDist = double.MaxValue; foreach (var candidate in candidates) { var node = _nodes.Values .FirstOrDefault(n => n.Location.Equals(candidate)); if (node != null) { double dist = GeoPoint.HaversineDistance( point, node.Location); if (dist < minDist) { minDist = dist; nearest = node; } } } return nearest; } public (List<string> path, double totalTimeSeconds) FindShortestPath( string startNodeId, string endNodeId) { var distances = new Dictionary<string, double>(); var previous = new Dictionary<string, string?>(); var visited = new HashSet<string>(); var pq = new SortedSet< (double dist, string nodeId)>(); foreach (var nodeId in _nodes.Keys) distances[nodeId] = double.MaxValue; distances[startNodeId] = 0; previous[startNodeId] = null; pq.Add((0, startNodeId)); while (pq.Count > 0) { var cur = pq.Min; pq.Remove(pq.Min); if (!visited.Add(cur.nodeId)) continue; if (cur.nodeId == endNodeId) break; if (!_nodes.TryGetValue( cur.nodeId, out var currentNode)) continue; foreach (var edge in currentNode.Edges) { if (visited.Contains( edge.ToNodeId)) continue; double edgeTime = edge.CurrentTimeSeconds; double newDist = cur.dist + edgeTime; if (newDist < distances.GetValueOrDefault( edge.ToNodeId, double.MaxValue)) { distances[edge.ToNodeId] = newDist; previous[edge.ToNodeId] = cur.nodeId; pq.Add((newDist, edge.ToNodeId)); } } } var path = ReconstructPath( previous, endNodeId); double totalTime = distances.GetValueOrDefault( endNodeId, double.MaxValue); return (path, totalTime); } public (List<string> path, double totalTimeSeconds) FindPathAStar( string startNodeId, string endNodeId) { if (!_nodes.ContainsKey(startNodeId) || !_nodes.ContainsKey(endNodeId)) return (new List<string>(), double.MaxValue); var gScore = new Dictionary<string, double>(); var fScore = new Dictionary<string, double>(); var previous = new Dictionary<string, string?>(); var closedSet = new HashSet<string>(); var openSet = new SortedSet< (double f, string nodeId)>(); gScore[startNodeId] = 0; fScore[startNodeId] = Heuristic(startNodeId, endNodeId); openSet.Add( (fScore[startNodeId], startNodeId)); while (openSet.Count > 0) { var (_, currentId) = openSet.Min; openSet.Remove(openSet.Min); if (currentId == endNodeId) break; closedSet.Add(currentId); if (!_nodes.TryGetValue( currentId, out var currentNode)) continue; foreach (var edge in currentNode.Edges) { if (closedSet.Contains( edge.ToNodeId)) continue; double tentativeG = gScore[currentId] + edge.CurrentTimeSeconds; double currentG = gScore.GetValueOrDefault( edge.ToNodeId, double.MaxValue); if (tentativeG < currentG) { previous[edge.ToNodeId] = currentId; gScore[edge.ToNodeId] = tentativeG; double f = tentativeG + Heuristic( edge.ToNodeId, endNodeId); fScore[edge.ToNodeId] = f; openSet.Add((f, edge.ToNodeId)); } } } var path = ReconstructPath( previous, endNodeId); double totalTime = gScore.GetValueOrDefault( endNodeId, double.MaxValue); return (path, totalTime); } private double Heuristic( string nodeId, string endNodeId) { if (_nodes.TryGetValue(nodeId, out var node) && _nodes.TryGetValue(endNodeId, out var endNode)) { return GeoPoint .HaversineDistance( node.Location, endNode.Location) / 33.3; } return 0; } private List<string> ReconstructPath( Dictionary<string, string?> previous, string endNodeId) { var path = new List<string>(); string? current = endNodeId; while (current != null) { path.Insert(0, current); previous.TryGetValue( current, out current); } return path.Count > 1 ? path : new List<string>(); } public int NodeCount => _nodes.Count; public int EdgeCount => _edges.Count; public IReadOnlyCollection<RoadNode> Nodes => _nodes.Values; } #endregion #region Map Tile System public enum TileFormat { Png, Webp, Pbf } public class MapTile { public int Zoom { get; set; } public int X { get; set; } public int Y { get; set; } public TileFormat Format { get; set; } public byte[] Data { get; set; } = Array.Empty<byte>(); public DateTime RenderedAt { get; set; } public long Version { get; set; } public string TileId => $"{Zoom}/{X}/{Y}"; public string ToUrl(string baseUrl) => $"{baseUrl}/tiles/{Zoom}/{X}/{Y}" + $".{Format.ToString().ToLower()}"; } public class TileCache { private readonly ConcurrentDictionary< string, MapTile> _l1Cache; private readonly int _l1MaxSize; public TileCache( int l1MaxSize = 10000) { _l1MaxSize = l1MaxSize; _l1Cache = new ConcurrentDictionary< string, MapTile>(); } public MapTile? GetTile( int zoom, int x, int y, TileFormat format) { string key = $"{zoom}/{x}/{y}/{format}"; return _l1Cache.TryGetValue( key, out var tile) ? tile : null; } public void PutTile(MapTile tile) { string key = tile.TileId + "/" + tile.Format; if (_l1Cache.Count >= _l1MaxSize) { var oldest = _l1Cache .OrderBy(kvp => kvp.Value.RenderedAt) .Take(_l1MaxSize / 4) .Select(kvp => kvp.Key) .ToList(); foreach (var k in oldest) _l1Cache.TryRemove(k, out _); } _l1Cache[key] = tile; } public List<MapTile> PrefetchRouteTiles( List<GeoPoint> routePolyline, int minZoom = 12, int maxZoom = 16) { var tiles = new List<MapTile>(); var visited = new HashSet<string>(); for (int zoom = minZoom; zoom <= maxZoom; zoom++) { foreach (var point in routePolyline) { var (tx, ty) = LatLngToTile( point.Latitude, point.Longitude, zoom); for (int dx = -2; dx <= 2; dx++) { for (int dy = -2; dy <= 2; dy++) { int x = tx + dx; int y = ty + dy; string key = $"{zoom}/{x}/{y}"; if (visited.Add(key)) { tiles.Add( new MapTile { Zoom = zoom, X = x, Y = y, Format = TileFormat.Pbf, RenderedAt = DateTime.UtcNow }); } } } } } return tiles; } public static (int x, int y) LatLngToTile( double lat, double lng, int zoom) { int n = (int)Math.Pow(2, zoom); int x = (int)((lng + 180.0) / 360.0 * n); int y = (int)( (1.0 - Math.Log( Math.Tan(lat * Math.PI / 180.0) + 1.0 / Math.Cos( lat * Math.PI / 180.0) ) / Math.PI) / 2.0 * n); return ( Math.Clamp(x, 0, n - 1), Math.Clamp(y, 0, n - 1)); } public int CacheSize => _l1Cache.Count; } #endregion #region Traffic System public class TrafficSegment { public string SegmentId { get; set; } = string.Empty; public DateTime RecordedAt { get; set; } public double CurrentSpeedKmh { get; set; } public double FreeFlowSpeedKmh { get; set; } public int SampleCount { get; set; } public string Source { get; set; } = "gps"; public double CongestionLevel => FreeFlowSpeedKmh > 0 ? Math.Clamp(1.0 - (CurrentSpeedKmh / FreeFlowSpeedKmh), 0, 1) : 0; public string TrafficLevel => CongestionLevel switch { < 0.25 => "GREEN", < 0.50 => "YELLOW", < 0.75 => "ORANGE", _ => "RED" }; } public class GpsPing { public string DeviceId { get; set; } = string.Empty; public GeoPoint Location { get; set; } public double SpeedKmh { get; set; } public double Heading { get; set; } public DateTime Timestamp { get; set; } public double AccuracyMeters { get; set; } } public class TrafficService { private readonly ConcurrentDictionary< string, List<TrafficSegment>> _segmentHistory; private readonly ConcurrentDictionary< string, TrafficSegment> _currentTraffic; private readonly RoadGraph _roadGraph; public TrafficService( RoadGraph roadGraph) { _roadGraph = roadGraph; _segmentHistory = new ConcurrentDictionary< string, List<TrafficSegment>>(); _currentTraffic = new ConcurrentDictionary< string, TrafficSegment>(); } public void ProcessGpsPing( GpsPing ping) { string? matchedSegment = MapMatchToSegment(ping); if (matchedSegment == null) return; var segment = _currentTraffic .GetOrAdd(matchedSegment, _ => new TrafficSegment { SegmentId = matchedSegment }); lock (segment) { double oldAvg = segment.CurrentSpeedKmh * segment.SampleCount; segment.SampleCount++; segment.CurrentSpeedKmh = (oldAvg + ping.SpeedKmh) / segment.SampleCount; segment.RecordedAt = DateTime.UtcNow; segment.Source = "gps"; } } public TrafficSegment? GetTrafficForSegment( string segmentId) { return _currentTraffic.TryGetValue( segmentId, out var segment) ? segment : null; } public List<TrafficSegment> GetTrafficForArea( BoundingBox bounds) { return _currentTraffic.Values .ToList(); } public void UpdateEdgeWeights() { foreach (var (segmentId, traffic) in _currentTraffic) { if ((DateTime.UtcNow - traffic.RecordedAt) .TotalMinutes > 5) continue; foreach (var node in _roadGraph.Nodes) { foreach (var edge in node.Edges) { if (edge.Id == segmentId) { edge.CurrentSpeedKmh = traffic .CurrentSpeedKmh; } } } } } private string? MapMatchToSegment( GpsPing ping) { var nearest = _roadGraph.FindNearestNode( ping.Location); if (nearest == null) return null; double dist = GeoPoint.HaversineDistance( ping.Location, nearest.Location); if (dist > 100) return null; return nearest.Edges .FirstOrDefault()?.Id; } public int ActiveSegmentCount => _currentTraffic.Count; } #endregion #region Place Search (POI) public class PlaceOfInterest { public string PlaceId { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public GeoPoint Location { get; set; } public string Category { get; set; } = string.Empty; public double Rating { get; set; } public int ReviewCount { get; set; } public double PopularityScore { get; set; } public bool IsOpenNow { get; set; } public string FormattedAddress { get; set; } = string.Empty; public double ComputeRelevanceScore( GeoPoint userLocation, string query) { double textScore = Name.Contains(query, StringComparison .OrdinalIgnoreCase) ? 1.0 : 0.3; double distance = GeoPoint.HaversineDistance( userLocation, Location); double proximityScore = Math.Exp(-distance / 1000); double popularityScore = PopularityScore / 100.0; double ratingScore = Rating / 5.0; return 0.35 * textScore + 0.25 * proximityScore + 0.20 * popularityScore + 0.10 * ratingScore + 0.10 * (IsOpenNow ? 1.0 : 0.3); } } public class PlaceSearchService { private readonly List< PlaceOfInterest> _places = new(); private readonly GeoCellIndex _placeIndex; public PlaceSearchService() { _placeIndex = new GeoCellIndex(level: 16); } public void AddPlace( PlaceOfInterest place) { _places.Add(place); _placeIndex.Insert(place.Location); } public List<PlaceOfInterest> Search( string query, GeoPoint userLocation, double radiusMeters = 5000, int maxResults = 20) { var nearbyPoints = _placeIndex.Query( userLocation, radiusMeters); return _places .Where(p => nearbyPoints.Contains( p.Location)) .Select(p => new { Place = p, Score = p .ComputeRelevanceScore( userLocation, query) }) .OrderByDescending( x => x.Score) .Take(maxResults) .Select(x => x.Place) .ToList(); } public List<PlaceOfInterest> Autocomplete( string prefix, GeoPoint userLocation, int maxResults = 5) { return _places .Where(p => p.Name.StartsWith(prefix, StringComparison .OrdinalIgnoreCase)) .OrderByDescending(p => p.ComputeRelevanceScore( userLocation, prefix)) .Take(maxResults) .ToList(); } public int TotalPlaces => _places.Count; } #endregion #region Navigation Engine public class NavigationInstruction { public int StepIndex { get; set; } public string Instruction { get; set; } = string.Empty; public string StreetName { get; set; } = string.Empty; public double DistanceMeters { get; set; } public double DurationSeconds { get; set; } public string ManeuverType { get; set; } = string.Empty; public GeoPoint StartPoint { get; set; } public GeoPoint EndPoint { get; set; } public string VoicePrompt => $"In {FormatDistance(DistanceMeters)}, " + $"{ManeuverType} onto {StreetName}"; private string FormatDistance( double meters) => meters < 1000 ? $"{meters:F0} meters" : $"{meters / 1000:F1} km"; } public class NavigationState { public enum NavStatus { Idle, RouteCalculated, Navigating, Rerouting, OffRoute, DestinationReached } public NavStatus Status { get; set; } = NavStatus.Idle; public List<string> RouteNodeIds { get; set; } = new(); public int CurrentStepIndex { get; set; } public GeoPoint CurrentPosition { get; set; } public double TotalDistanceMeters { get; set; } public double ElapsedTimeSeconds { get; set; } public double RemainingTimeSeconds { get; set; } public double SpeedKmh { get; set; } } public class NavigationEngine { private readonly RoadGraph _roadGraph; private readonly TrafficService _trafficService; private NavigationState _state; private List<NavigationInstruction> _instructions; public NavigationEngine( RoadGraph roadGraph, TrafficService trafficService) { _roadGraph = roadGraph; _trafficService = trafficService; _state = new NavigationState(); _instructions = new List<NavigationInstruction>(); } public NavigationState StartNavigation( GeoPoint origin, GeoPoint destination) { var startNode = _roadGraph.FindNearestNode( origin); var endNode = _roadGraph.FindNearestNode( destination); if (startNode == null || endNode == null) { _state.Status = NavigationState .NavStatus.OffRoute; return _state; } _trafficService.UpdateEdgeWeights(); var (path, totalTime) = _roadGraph.FindPathAStar( startNode.Id, endNode.Id); if (path.Count == 0) { _state.Status = NavigationState .NavStatus.OffRoute; return _state; } _state.RouteNodeIds = path; _state.CurrentStepIndex = 0; _state.CurrentPosition = origin; _state.Status = NavigationState .NavStatus.Navigating; _state.ElapsedTimeSeconds = 0; _state.RemainingTimeSeconds = totalTime; _instructions = GenerateInstructions(path); _state.TotalDistanceMeters = CalculateTotalDistance(path); return _state; } public NavigationState UpdatePosition( GeoPoint newPosition, double speedKmh) { if (_state.Status != NavigationState .NavStatus.Navigating) return _state; _state.CurrentPosition = newPosition; _state.SpeedKmh = speedKmh; _state.ElapsedTimeSeconds += 2; bool isOffRoute = CheckIfOffRoute(newPosition); if (isOffRoute) { _state.Status = NavigationState .NavStatus.Rerouting; return _state; } UpdateCurrentStep(newPosition); UpdateRemainingTime(); if (_state.CurrentStepIndex >= _instructions.Count) { _state.Status = NavigationState .NavStatus .DestinationReached; } return _state; } public NavigationState Reroute( GeoPoint currentPosition, GeoPoint destination) { return StartNavigation( currentPosition, destination); } public NavigationInstruction? GetCurrentInstruction() { if (_state.CurrentStepIndex < _instructions.Count) return _instructions[ _state.CurrentStepIndex]; return null; } public List<NavigationInstruction> GetAllInstructions() => new(_instructions); private List<NavigationInstruction> GenerateInstructions( List<string> path) { var instructions = new List<NavigationInstruction>(); for (int i = 1; i < path.Count; i++) { if (!_roadGraph.Nodes.Any( n => n.Id == path[i - 1])) continue; var fromNode = _roadGraph.Nodes.First( n => n.Id == path[i - 1]); var toNode = _roadGraph.Nodes.FirstOrDefault( n => n.Id == path[i]); if (toNode == null) continue; var edge = fromNode.Edges .FirstOrDefault( e => e.ToNodeId == path[i]); if (edge == null) continue; double bearing = CalculateBearing( fromNode.Location, toNode.Location); string maneuver = GetManeuverType(bearing, i < path.Count - 1); instructions.Add( new NavigationInstruction { StepIndex = i - 1, Instruction = $"Continue on " + $"{edge.RoadName}", StreetName = edge.RoadName, DistanceMeters = edge.DistanceMeters, DurationSeconds = edge.CurrentTimeSeconds, ManeuverType = maneuver, StartPoint = fromNode.Location, EndPoint = toNode.Location }); } return instructions; } private bool CheckIfOffRoute( GeoPoint position) { if (_state.CurrentStepIndex >= _instructions.Count) return false; var currentInstruction = _instructions[ _state.CurrentStepIndex]; double distToStep = GeoPoint.HaversineDistance( position, currentInstruction.EndPoint); bool isNearAnyStep = _instructions.Any(inst => GeoPoint.HaversineDistance( position, inst.StartPoint) < 100 || GeoPoint.HaversineDistance( position, inst.EndPoint) < 100); return distToStep > 200 && !isNearAnyStep; } private void UpdateCurrentStep( GeoPoint position) { while (_state.CurrentStepIndex < _instructions.Count - 1) { var nextStep = _instructions[ _state.CurrentStepIndex + 1]; double distToNext = GeoPoint.HaversineDistance( position, nextStep.StartPoint); if (distToNext < 50) _state.CurrentStepIndex++; else break; } } private void UpdateRemainingTime() { double remaining = 0; for (int i = _state.CurrentStepIndex; i < _instructions.Count; i++) { remaining += _instructions[i] .DurationSeconds; } _state.RemainingTimeSeconds = remaining; } private double CalculateTotalDistance( List<string> path) { double total = 0; for (int i = 1; i < path.Count; i++) { var fromNode = _roadGraph.Nodes.FirstOrDefault( n => n.Id == path[i - 1]); var toNode = _roadGraph.Nodes.FirstOrDefault( n => n.Id == path[i]); if (fromNode != null && toNode != null) { total += GeoPoint.HaversineDistance( fromNode.Location, toNode.Location); } } return total; } private double CalculateBearing( GeoPoint from, GeoPoint to) { double dLon = (to.Longitude - from.Longitude) * Math.PI / 180.0; double lat1 = from.Latitude * Math.PI / 180.0; double lat2 = to.Latitude * Math.PI / 180.0; double y = Math.Sin(dLon) * Math.Cos(lat2); double x = Math.Cos(lat1) * Math.Sin(lat2) - Math.Sin(lat1) * Math.Cos(lat2) * Math.Cos(dLon); double bearing = Math.Atan2(y, x) * 180.0 / Math.PI; return (bearing + 360) % 360; } private string GetManeuverType( double bearing, bool hasMoreSteps) { if (!hasMoreSteps) return "arrive at destination"; return bearing switch { >= 337.5 or < 22.5 => "go straight", >= 22.5 and < 67.5 => "turn slight right", >= 67.5 and < 112.5 => "turn right", >= 112.5 and < 157.5 => "turn sharp right", >= 157.5 and < 202.5 => "make a U-turn", >= 202.5 and < 247.5 => "turn sharp left", >= 247.5 and < 292.5 => "turn left", >= 292.5 and < 337.5 => "turn slight left", _ => "continue" }; } } #endregion }

25. Conclusion

Designing a Google Maps-like system is one of the most challenging and rewarding system design exercises. It touches virtually every aspect of distributed systems: geospatial indexing, real-time stream processing, machine learning pipelines, CDN optimization, database sharding, multi-region replication, and client-side rendering optimization.

The key takeaways from this deep dive are:

  • Geospatial indexing is fundamental: Standard B-tree indexes are insufficient for 2D proximity queries. Use S2 cells or Geohash for efficient spatial lookups.
  • Contraction Hierarchies enable real-time routing: Pre-processing the road graph with shortcut edges reduces query time from seconds to microseconds.
  • CDN-first architecture for tiles: Map tiles are immutable and cache perfectly at edge locations, achieving 99%+ cache hit rates.
  • Real-time traffic requires stream processing: Kafka + Flink pipelines process millions of GPS pings per second into traffic overlays within 30 seconds.
  • ETA prediction is an ML problem: Combining historical patterns, real-time conditions, weather, and events with Temporal Fusion Transformers achieves 95%+ accuracy.
  • Geo-sharding distributes data globally: Partition by geographic regions using S2 cells so that local queries hit a single shard.
  • Multi-region design provides resilience: Active-active tile serving across regions ensures 99.99% availability with sub-100ms latency.

For system design interviews, this topic allows you to demonstrate breadth (covering indexing, routing, traffic, tiles, caching, sharding) and depth (explaining Contraction Hierarchies, HMM map matching, Temporal Fusion Transformers). Focus on the trade-offs: latency vs accuracy in ETA prediction, consistency vs availability in multi-region traffic data, and pre-computation vs real-time computation in routing.

The maps and navigation space continues to evolve with autonomous driving (HD maps, V2X communication), indoor mapping (Bluetooth beacons, WiFi RTT), and immersive experiences (AR navigation with Live View). Understanding the foundational systems described in this article will prepare you to design the next generation of geospatial platforms.

Key System Design Principles from Google Maps:
  1. Pre-compute what you can (tiles, contraction hierarchies, traffic patterns) to serve queries in milliseconds
  2. Cache aggressively at every layer — tiles are the perfect caching use case (immutable, content-addressed)
  3. Use ML models to predict what cannot be pre-computed (traffic, ETA, search ranking)
  4. Shard by geography to keep local queries fast and minimize cross-shard communication
  5. Design for offline-first: mobile users frequently lose connectivity, so client-side caching is essential
  6. Process streaming data in real-time with exactly-once semantics for traffic accuracy
  7. Always maintain multiple fallback strategies (CDN failover, route alternatives, offline maps)

Written by Ayodhyya

System Design | Maps & Navigation | Distributed Systems

Copyright 2026 Ayodhyya. All rights reserved.