I need to split /root/traccar-geocoder/builder/src/build_index.cpp (2740 lines) into separate files. The code has these logical sections:

1. S2 helpers (cover_edge, point_to_cell, cover_polygon) - lines ~240-340
2. AdminCoverPool thread pool class - lines ~150-245
3. ParsedData container struct - lines ~245-280
4. Ring assembly (assemble_outer_rings + BacktrackState) - lines ~345-570
5. add_addr_point, add_admin_polygon helper functions - lines ~580-620
6. RelationCollector osmium handler - lines ~625-690
7. resolve_interpolation_endpoints - lines ~700-760
8. deduplicate template - line ~765
9. Cache serialization (serialize/deserialize) - lines ~775-1000
10. Continent bbox filtering - lines ~1000-1250
11. Cell index writing (write_entries, write_entries_from_sorted, write_cell_index, write_index) - lines ~1270-1610
12. main() with PBF processing pipeline - lines ~1615-2740

The project uses CMake with a single target. Current headers: types.h, string_pool.h, geometry.h (all header-only).

Plan the best way to split this. Consider:
- What should be header-only vs separate .cpp files?
- How to handle dependencies between modules (ParsedData is used everywhere)
- Keep the CMake changes minimal
- The code uses osmium and S2 libraries which have heavy includes

Read the current CMakeLists.txt at /root/traccar-geocoder/builder/CMakeLists.txt and the build_index.cpp file to understand the dependencies, then propose a split.


I'll start by reading the CMakeLists.txt and the build_index.cpp file to understand the current structure and dependencies.


     1→cmake_minimum_required(VERSION 3.16)
     2→project(traccar-geocoder-build LANGUAGES CXX)
     3→
     4→set(CMAKE_CXX_STANDARD 17)
     5→set(CMAKE_CXX_STANDARD_REQUIRED ON)
     6→
     7→find_package(Threads REQUIRED)
     8→find_package(ZLIB REQUIRED)
     9→find_package(BZip2 REQUIRED)
    10→find_package(EXPAT REQUIRED)
    11→find_package(s2 QUIET)
    12→
    13→add_executable(build-index src/build_index.cpp)
    14→
    15→if(EXISTS /opt/homebrew/include)
    16→    target_include_directories(build-index PRIVATE /opt/homebrew/include)
    17→endif()
    18→
    19→if(s2_FOUND)
    20→    target_link_libraries(build-index PRIVATE
    21→        s2::s2
    22→        ZLIB::ZLIB
    23→        BZip2::BZip2
    24→        EXPAT::EXPAT
    25→        Threads::Threads
    26→    )
    27→else()
    28→    target_include_directories(build-index PRIVATE /usr/include/s2)
    29→    target_link_libraries(build-index PRIVATE
    30→        s2
    31→        ZLIB::ZLIB
    32→        BZip2::BZip2
    33→        EXPAT::EXPAT
    34→        Threads::Threads
    35→    )
    36→endif()
    37→


     1→#include <algorithm>
     2→#include <chrono>
     3→#include <atomic>
     4→#include <cmath>
     5→#include <condition_variable>
     6→#include <cstdint>
     7→#include <cstring>
     8→#include <deque>
     9→#include <fstream>
    10→#include <future>
    11→#include <iostream>
    12→#include <mutex>
    13→#include <set>
    14→#include <string>
    15→#include <thread>
    16→#include <unordered_map>
    17→#include <unordered_set>
    18→#include <vector>
    19→
    20→#include <osmium/handler.hpp>
    21→#include <osmium/io/pbf_input.hpp>
    22→#include <osmium/visitor.hpp>
    23→#include <fcntl.h>
    24→#include <unistd.h>
    25→#include <sys/mman.h>
    26→#include <sys/stat.h>
    27→
    28→#include <s2/s2cell_id.h>
    29→#include <s2/s2latlng.h>
    30→#include <s2/s2region_coverer.h>
    31→#include <s2/s2polyline.h>
    32→#include <s2/s2polygon.h>
    33→#include <s2/s2loop.h>
    34→#include <s2/s2builder.h>
    35→
    36→#include "types.h"
    37→#include "string_pool.h"
    38→#include "geometry.h"
    39→
    40→// --- Directory creation ---
    41→
    42→static void ensure_dir(const std::string& path) {
    43→    mkdir(path.c_str(), 0755);
    44→}
    45→
    46→// Types, constants, and IndexMode are in types.h
    47→
    48→// StringPool is in string_pool.h
    49→
    50→// --- Phase timer ---
    51→static void log_phase(const char* name, std::chrono::steady_clock::time_point& t) {
    52→    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
    53→        std::chrono::steady_clock::now() - t).count();
    54→    std::cerr << "  [" << ms/1000 << "." << (ms%1000)/100 << "s] " << name << std::endl;
    55→    t = std::chrono::steady_clock::now();
    56→}
    57→
    58→// DeferredWay, DeferredInterp, CollectedRelation are in types.h
    59→
    60→// Forward declaration
    61→static std::vector<std::pair<S2CellId, bool>> cover_polygon(const std::vector<std::pair<double,double>>& vertices);
    62→
    63→// --- Thread pool for concurrent admin polygon S2 covering ---
    64→
    65→class AdminCoverPool {
    66→public:
    67→    struct WorkItem {
    68→        uint32_t poly_id;
    69→        std::vector<std::pair<double,double>> vertices;
    70→    };
    71→
    72→    explicit AdminCoverPool(size_t num_threads) : stop_(false) {
    73→        for (size_t i = 0; i < num_threads; i++) {
    74→            workers_.emplace_back([this]() { worker_loop(); });
    75→        }
    76→    }
    77→
    78→    ~AdminCoverPool() {
    79→        {
    80→            std::lock_guard<std::mutex> lock(mutex_);
    81→            stop_ = true;
    82→        }
    83→        cv_.notify_all();
    84→        for (auto& t : workers_) t.join();
    85→    }
    86→
    87→    void submit(uint32_t poly_id, std::vector<std::pair<double,double>>&& vertices) {
    88→        {
    89→            std::lock_guard<std::mutex> lock(mutex_);
    90→            queue_.push_back({poly_id, std::move(vertices)});
    91→        }
    92→        cv_.notify_one();
    93→    }
    94→
    95→    // Wait for all pending work to complete, then return merged results
    96→    std::unordered_map<uint64_t, std::vector<uint32_t>> drain() {
    97→        // Wait until queue is empty and all workers are idle
    98→        {
    99→            std::unique_lock<std::mutex> lock(mutex_);
   100→            done_cv_.wait(lock, [this]() { return queue_.empty() && active_workers_ == 0; });
   101→        }
   102→
   103→        // Merge all thread-local results
   104→        std::unordered_map<uint64_t, std::vector<uint32_t>> merged;
   105→        for (auto& local : thread_results_) {
   106→            for (auto& [cell_id, ids] : local) {
   107→                auto& target = merged[cell_id];
   108→                target.insert(target.end(), ids.begin(), ids.end());
   109→            }
   110→        }
   111→        return merged;
   112→    }
   113→
   114→private:
   115→    void worker_loop() {
   116→        size_t my_idx;
   117→        {
   118→            std::lock_guard<std::mutex> lock(mutex_);
   119→            my_idx = thread_results_.size();
   120→            thread_results_.emplace_back();
   121→        }
   122→
   123→        while (true) {
   124→            WorkItem item;
   125→            {
   126→                std::unique_lock<std::mutex> lock(mutex_);
   127→                cv_.wait(lock, [this]() { return stop_ || !queue_.empty(); });
   128→                if (stop_ && queue_.empty()) return;
   129→                item = std::move(queue_.front());
   130→                queue_.pop_front();
   131→                active_workers_++;
   132→            }
   133→
   134→            // Compute S2 covering (no locks needed — pure computation)
   135→            auto cell_ids = cover_polygon(item.vertices);
   136→            auto& local = thread_results_[my_idx];
   137→            for (const auto& [cell_id, is_interior] : cell_ids) {
   138→                uint32_t entry = is_interior ? (item.poly_id | INTERIOR_FLAG) : item.poly_id;
   139→                local[cell_id.id()].push_back(entry);
   140→            }
   141→
   142→            {
   143→                std::lock_guard<std::mutex> lock(mutex_);
   144→                active_workers_--;
   145→            }
   146→            done_cv_.notify_all();
   147→        }
   148→    }
   149→
   150→    std::vector<std::thread> workers_;


   150→    std::vector<std::thread> workers_;
   151→    std::deque<WorkItem> queue_;
   152→    std::vector<std::unordered_map<uint64_t, std::vector<uint32_t>>> thread_results_;
   153→    std::mutex mutex_;
   154→    std::condition_variable cv_;
   155→    std::condition_variable done_cv_;
   156→    size_t active_workers_ = 0;
   157→    bool stop_;
   158→};
   159→
   160→// --- Parsed data container ---
   161→
   162→// CellItemPair is in types.h
   163→
   164→struct ParsedData {
   165→    StringPool string_pool;
   166→    std::vector<WayHeader> ways;
   167→    std::vector<NodeCoord> street_nodes;
   168→    std::unordered_map<uint64_t, std::vector<uint32_t>> cell_to_ways;
   169→    std::vector<AddrPoint> addr_points;
   170→    std::unordered_map<uint64_t, std::vector<uint32_t>> cell_to_addrs;
   171→    std::vector<InterpWay> interp_ways;
   172→    std::vector<NodeCoord> interp_nodes;
   173→    std::unordered_map<uint64_t, std::vector<uint32_t>> cell_to_interps;
   174→    std::vector<AdminPolygon> admin_polygons;
   175→    std::vector<NodeCoord> admin_vertices;
   176→    std::unordered_map<uint64_t, std::vector<uint32_t>> cell_to_admin;
   177→
   178→    // Deferred work for parallel S2 computation (ways + interps)
   179→    std::vector<DeferredWay> deferred_ways;
   180→    std::vector<DeferredInterp> deferred_interps;
   181→
   182→    // Sorted (cell_id, item_id) pairs — kept for direct entry writing
   183→    std::vector<CellItemPair> sorted_way_cells;
   184→    std::vector<CellItemPair> sorted_addr_cells;
   185→
   186→    // Collected data for parallel admin assembly
   187→    std::vector<CollectedRelation> collected_relations;
   188→    struct WayGeometry {
   189→        std::vector<std::pair<double,double>> coords;
   190→        int64_t first_node_id;
   191→        int64_t last_node_id;
   192→    };
   193→    std::unordered_map<int64_t, WayGeometry> way_geometries;
   194→};
   195→
   196→// --- S2 helpers ---
   197→
   198→static int kStreetCellLevel = 17;
   199→static int kAdminCellLevel = 10;
   200→static int kMaxAdminLevel = 0;  // 0 means no filtering
   201→
   202→// Fast cover_edge: compute cells covered by a line segment.
   203→// Fast path for short edges (same cell or adjacent) avoids S2RegionCoverer entirely.
   204→// ~80% of street edges are within 1-2 cells, so this eliminates most S2 library calls.
   205→static void cover_edge(double lat1, double lng1, double lat2, double lng2,
   206→                        std::vector<S2CellId>& out) {
   207→    out.clear();
   208→
   209→    // Fast: compute cell IDs directly from coordinates
   210→    S2CellId c1 = S2CellId(S2LatLng::FromDegrees(lat1, lng1)).parent(kStreetCellLevel);
   211→    S2CellId c2 = S2CellId(S2LatLng::FromDegrees(lat2, lng2)).parent(kStreetCellLevel);
   212→
   213→    // Same cell — most common case for short edges
   214→    if (c1 == c2) {
   215→        out.push_back(c1);
   216→        return;
   217→    }
   218→
   219→    // Adjacent cells — check midpoint
   220→    S2CellId cm = S2CellId(S2LatLng::FromDegrees((lat1+lat2)/2, (lng1+lng2)/2)).parent(kStreetCellLevel);
   221→    if (cm == c1 || cm == c2) {
   222→        out.push_back(c1);
   223→        out.push_back(c2);
   224→        return;
   225→    }
   226→
   227→    // Longer edge — use S2RegionCoverer for correctness (handles geodesic curves)
   228→    thread_local S2RegionCoverer coverer = []() {
   229→        S2RegionCoverer::Options options;
   230→        options.set_fixed_level(kStreetCellLevel);
   231→        return S2RegionCoverer(options);
   232→    }();
   233→
   234→    S2Point p1 = S2LatLng::FromDegrees(lat1, lng1).ToPoint();
   235→    S2Point p2 = S2LatLng::FromDegrees(lat2, lng2).ToPoint();
   236→    if (p1 == p2) { out.push_back(c1); return; }
   237→
   238→    std::vector<S2Point> points = {p1, p2};
   239→    S2Polyline polyline(points);
   240→    S2CellUnion covering = coverer.GetCovering(polyline);
   241→    out = std::move(covering.cell_ids());
   242→}
   243→
   244→// Legacy overload for code that still uses return-by-value
   245→static std::vector<S2CellId> cover_edge(double lat1, double lng1, double lat2, double lng2) {
   246→    std::vector<S2CellId> result;
   247→    cover_edge(lat1, lng1, lat2, lng2, result);
   248→    return result;
   249→}
   250→
   251→static S2CellId point_to_cell(double lat, double lng) {
   252→    return S2CellId(S2LatLng::FromDegrees(lat, lng)).parent(kStreetCellLevel);
   253→}
   254→
   255→// polygon_area is in geometry.h
   256→
   257→// Returns pairs of (cell_id, is_interior)
   258→static std::vector<std::pair<S2CellId, bool>> cover_polygon(const std::vector<std::pair<double,double>>& vertices) {
   259→    std::vector<S2Point> points;
   260→    points.reserve(vertices.size());
   261→    for (const auto& [lat, lng] : vertices) {
   262→        S2Point p = S2LatLng::FromDegrees(lat, lng).ToPoint();
   263→        if (!points.empty() && points.back() == p) continue;
   264→        points.push_back(p);
   265→    }
   266→    // Remove closing duplicate if first == last
   267→    if (points.size() > 1 && points.front() == points.back()) {
   268→        points.pop_back();
   269→    }
   270→    if (points.size() < 3) return {};
   271→
   272→    // Build S2Loop (must be CCW), skip invalid polygons
   273→    S2Error error;
   274→    auto loop = std::make_unique<S2Loop>(points, S2Debug::DISABLE);
   275→    loop->Normalize();
   276→    if (loop->FindValidationError(&error)) return {};
   277→
   278→    S2Polygon polygon(std::move(loop));
   279→
   280→    S2RegionCoverer::Options options;
   281→    options.set_max_level(kAdminCellLevel);
   282→    options.set_max_cells(MAX_S2_CELLS_PER_POLY);
   283→
   284→    S2RegionCoverer coverer(options);
   285→    S2CellUnion covering = coverer.GetCovering(polygon);
   286→    S2CellUnion interior = coverer.GetInteriorCovering(polygon);
   287→
   288→    // Build set of interior cell IDs for fast lookup
   289→    std::unordered_set<uint64_t> interior_set;
   290→    for (const auto& cell : interior.cell_ids()) {
   291→        if (cell.level() <= kAdminCellLevel) {
   292→            auto begin = cell.range_min().parent(kAdminCellLevel);
   293→            auto end = cell.range_max().parent(kAdminCellLevel);
   294→            for (auto c = begin; c != end; c = c.next()) {
   295→                interior_set.insert(c.id());
   296→            }
   297→            interior_set.insert(end.id());
   298→        } else {
   299→            interior_set.insert(cell.parent(kAdminCellLevel).id());
   300→        }
   301→    }
   302→
   303→    // Normalize all covering cells to kAdminCellLevel
   304→    std::vector<std::pair<S2CellId, bool>> result;
   305→    for (const auto& cell : covering.cell_ids()) {
   306→        if (cell.level() <= kAdminCellLevel) {
   307→            auto begin = cell.range_min().parent(kAdminCellLevel);
   308→            auto end = cell.range_max().parent(kAdminCellLevel);
   309→            for (auto c = begin; c != end; c = c.next()) {
   310→                result.emplace_back(c, interior_set.count(c.id()) > 0);
   311→            }
   312→            result.emplace_back(end, interior_set.count(end.id()) > 0);
   313→        } else {
   314→            auto parent = cell.parent(kAdminCellLevel);
   315→            result.emplace_back(parent, interior_set.count(parent.id()) > 0);
   316→        }
   317→    }
   318→
   319→    // Deduplicate by cell_id, keeping interior=true if any duplicate is interior
   320→    std::sort(result.begin(), result.end(), [](const auto& a, const auto& b) {
   321→        return a.first < b.first;
   322→    });
   323→    auto it = result.begin();
   324→    for (auto curr = result.begin(); curr != result.end(); ) {
   325→        auto next = curr + 1;
   326→        bool is_interior = curr->second;
   327→        while (next != result.end() && next->first == curr->first) {
   328→            is_interior = is_interior || next->second;
   329→            ++next;
   330→        }
   331→        *it = {curr->first, is_interior};
   332→        ++it;
   333→        curr = next;
   334→    }
   335→    result.erase(it, result.end());
   336→    return result;
   337→}
   338→
   339→// dp_simplify, simplify_polygon are in geometry.h
   340→
   341→// segments_intersect, ring_has_self_intersection, ring_has_duplicate_coords are in geometry.h
   342→
   343→// --- Parallel admin: assemble outer rings from collected way geometries ---
   344→
   345→static std::vector<std::vector<std::pair<double,double>>> assemble_outer_rings(
   346→    const std::vector<std::pair<int64_t, std::string>>& members,
   347→    const std::unordered_map<int64_t, ParsedData::WayGeometry>& way_geoms,
   348→    bool include_all_roles = false)
   349→{


     1→#pragma once
     2→
     3→#include <cstdint>
     4→#include <vector>
     5→
     6→// --- Binary format structs (must match server's index reader) ---
     7→
     8→struct WayHeader {
     9→    uint32_t node_offset;
    10→    uint8_t node_count;
    11→    uint32_t name_id;
    12→};
    13→
    14→struct AddrPoint {
    15→    float lat;
    16→    float lng;
    17→    uint32_t housenumber_id;
    18→    uint32_t street_id;
    19→};
    20→
    21→struct InterpWay {
    22→    uint32_t node_offset;
    23→    uint8_t node_count;
    24→    uint32_t street_id;
    25→    uint32_t start_number;
    26→    uint32_t end_number;
    27→    uint8_t interpolation;
    28→};
    29→
    30→struct AdminPolygon {
    31→    uint32_t vertex_offset;
    32→    uint16_t vertex_count;
    33→    uint32_t name_id;
    34→    uint8_t admin_level;
    35→    float area;
    36→    uint16_t country_code;
    37→};
    38→
    39→struct NodeCoord {
    40→    float lat;
    41→    float lng;
    42→};
    43→
    44→// Sorted cell-item pair for direct entry writing (avoids hash map)
    45→struct CellItemPair {
    46→    uint64_t cell_id;
    47→    uint32_t item_id;
    48→};
    49→
    50→// --- Constants ---
    51→
    52→static const uint32_t INTERIOR_FLAG = 0x80000000u;
    53→static const uint32_t ID_MASK       = 0x7FFFFFFFu;
    54→static const uint32_t NO_DATA       = 0xFFFFFFFFu;
    55→
    56→// Processing limits
    57→static constexpr size_t MAX_BLOCK_QUEUE         = 64;    // producer-consumer queue depth
    58→static constexpr int    MAX_NODE_COUNT          = 255;   // uint8_t node_count
    59→static constexpr int    MAX_VERTEX_COUNT        = 65535; // uint16_t vertex_count
    60→static constexpr int    MAX_S2_CELLS_PER_POLY   = 200;   // S2RegionCoverer max_cells
    61→static constexpr int    BACKTRACK_CALL_BUDGET   = 100000; // per-ring backtracking limit
    62→static constexpr int    BACKTRACK_TOTAL_BUDGET  = 500000; // per-relation backtracking limit
    63→static constexpr int    BACKTRACK_MAX_DEPTH     = 200;    // recursion depth limit
    64→static constexpr size_t MAX_SUBWAYS_FOR_RETRY   = 30;     // only retry small relations
    65→static constexpr size_t MAX_NODE_ID_DEFAULT     = 15000000000ULL; // dense index capacity
    66→
    67→// --- Index mode ---
    68→
    69→enum class IndexMode { Full, NoAddresses, AdminOnly };
    70→
    71→// --- Deferred S2 work items ---
    72→
    73→struct DeferredWay {
    74→    uint32_t way_id;
    75→    uint32_t node_offset;
    76→    uint8_t node_count;
    77→};
    78→
    79→struct DeferredInterp {
    80→    uint32_t interp_id;
    81→    uint32_t node_offset;
    82→    uint8_t node_count;
    83→};
    84→
    85→// Collected relation data for parallel admin assembly
    86→struct CollectedRelation {
    87→    int64_t id;
    88→    uint8_t admin_level;
    89→    std::string name;
    90→    std::string country_code;
    91→    bool is_postal;
    92→    std::vector<std::pair<int64_t, std::string>> members; // (way_id, role)
    93→};
    94→


     1→#pragma once
     2→
     3→#include <cstdint>
     4→#include <string>
     5→#include <unordered_map>
     6→#include <vector>
     7→
     8→class StringPool {
     9→public:
    10→    uint32_t intern(const std::string& s) {
    11→        auto it = index_.find(s);
    12→        if (it != index_.end()) {
    13→            return it->second;
    14→        }
    15→        uint32_t offset = static_cast<uint32_t>(data_.size());
    16→        index_[s] = offset;
    17→        data_.insert(data_.end(), s.begin(), s.end());
    18→        data_.push_back('\0');
    19→        return offset;
    20→    }
    21→
    22→    const std::vector<char>& data() const { return data_; }
    23→    std::vector<char>& mutable_data() { return data_; }
    24→
    25→private:
    26→    std::unordered_map<std::string, uint32_t> index_;
    27→    std::vector<char> data_;
    28→};
    29→


     1→#pragma once
     2→
     3→#include <algorithm>
     4→#include <cmath>
     5→#include <cstdint>
     6→#include <functional>
     7→#include <unordered_set>
     8→#include <vector>
     9→
    10→// Coordinate key matching osmium's Location (int32_t nanodegrees).
    11→// Two points at the same geographic location always produce the same key,
    12→// regardless of their OSM node IDs (handles split/replaced nodes).
    13→inline int64_t coord_key(double lat, double lng) {
    14→    int32_t ilat = static_cast<int32_t>(lat * 1e7 + (lat >= 0 ? 0.5 : -0.5));
    15→    int32_t ilng = static_cast<int32_t>(lng * 1e7 + (lng >= 0 ? 0.5 : -0.5));
    16→    return (static_cast<int64_t>(ilat) << 32) | static_cast<uint32_t>(ilng);
    17→}
    18→
    19→// Approximate polygon area in square degrees (shoelace formula)
    20→inline float polygon_area(const std::vector<std::pair<double,double>>& vertices) {
    21→    double area = 0;
    22→    size_t n = vertices.size();
    23→    for (size_t i = 0; i < n; i++) {
    24→        size_t j = (i + 1) % n;
    25→        area += vertices[i].first * vertices[j].second;
    26→        area -= vertices[j].first * vertices[i].second;
    27→    }
    28→    return static_cast<float>(std::fabs(area) / 2.0);
    29→}
    30→
    31→// Check if segment (a1,a2) crosses segment (b1,b2) using cross product orientation
    32→inline bool segments_intersect(double ax1, double ay1, double ax2, double ay2,
    33→                               double bx1, double by1, double bx2, double by2) {
    34→    auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) -> double {
    35→        return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);
    36→    };
    37→    double d1 = cross(bx1, by1, bx2, by2, ax1, ay1);
    38→    double d2 = cross(bx1, by1, bx2, by2, ax2, ay2);
    39→    double d3 = cross(ax1, ay1, ax2, ay2, bx1, by1);
    40→    double d4 = cross(ax1, ay1, ax2, ay2, bx2, by2);
    41→    if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
    42→        ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
    43→        return true;
    44→    }
    45→    return false;
    46→}
    47→
    48→// Sweep-line inspired self-intersection check for rings.
    49→// Brute force for small rings (<=32 segments), sweep-line pruning for large ones.
    50→inline bool ring_has_self_intersection(const std::vector<std::pair<double,double>>& ring) {
    51→    static constexpr size_t BRUTE_FORCE_THRESHOLD = 32;
    52→    size_t n = ring.size();
    53→    if (n < 4) return false;
    54→
    55→    if (n <= BRUTE_FORCE_THRESHOLD) {
    56→        for (size_t i = 0; i + 1 < n; i++) {
    57→            for (size_t j = i + 2; j + 1 < n; j++) {
    58→                if (j == i + 1 || (i == 0 && j == n - 2)) continue;
    59→                if (segments_intersect(ring[i].first, ring[i].second,
    60→                                       ring[i+1].first, ring[i+1].second,
    61→                                       ring[j].first, ring[j].second,
    62→                                       ring[j+1].first, ring[j+1].second))
    63→                    return true;
    64→            }
    65→        }
    66→        return false;
    67→    }
    68→
    69→    // For larger rings, sort segments by min-x and prune non-overlapping
    70→    struct Seg { double min_x, max_x; size_t idx; };
    71→    std::vector<Seg> segs(n - 1);
    72→    for (size_t i = 0; i + 1 < n; i++) {
    73→        double x1 = ring[i].second, x2 = ring[i+1].second; // use lng as x
    74→        segs[i] = {std::min(x1,x2), std::max(x1,x2), i};
    75→    }
    76→    std::sort(segs.begin(), segs.end(), [](const Seg& a, const Seg& b) {
    77→        return a.min_x < b.min_x;
    78→    });
    79→
    80→    for (size_t a = 0; a + 1 < segs.size(); a++) {
    81→        for (size_t b = a + 1; b < segs.size(); b++) {
    82→            if (segs[b].min_x > segs[a].max_x) break;
    83→            size_t i = segs[a].idx, j = segs[b].idx;
    84→            if (j == i + 1 || i == j + 1) continue;
    85→            if ((i == 0 && j == n - 2) || (j == 0 && i == n - 2)) continue;
    86→            if (segments_intersect(ring[i].first, ring[i].second,
    87→                                   ring[i+1].first, ring[i+1].second,
    88→                                   ring[j].first, ring[j].second,
    89→                                   ring[j+1].first, ring[j+1].second))
    90→                return true;
    91→        }
    92→    }
    93→    return false;
    94→}
    95→
    96→// Check if a ring has duplicate coordinates (spikes/figure-8 from merged holes).
    97→// A valid simple polygon visits each coordinate exactly once (except closing point).
    98→inline bool ring_has_duplicate_coords(const std::vector<std::pair<double,double>>& ring) {
    99→    std::unordered_set<int64_t> seen;
   100→    for (size_t i = 0; i + 1 < ring.size(); i++) {
   101→        int64_t k = coord_key(ring[i].first, ring[i].second);
   102→        if (!seen.insert(k).second) return true;
   103→    }
   104→    return false;
   105→}
   106→
   107→// Douglas-Peucker line simplification
   108→inline void dp_simplify(const std::vector<std::pair<double,double>>& pts,
   109→                        size_t start, size_t end, double epsilon,
   110→                        std::vector<bool>& keep) {
   111→    if (end <= start + 1) return;
   112→
   113→    double max_dist = 0;
   114→    size_t max_idx = start;
   115→
   116→    double ax = pts[start].first, ay = pts[start].second;
   117→    double bx = pts[end].first, by = pts[end].second;
   118→    double dx = bx - ax, dy = by - ay;
   119→    double len_sq = dx * dx + dy * dy;
   120→
   121→    for (size_t i = start + 1; i < end; i++) {
   122→        double px = pts[i].first - ax, py = pts[i].second - ay;
   123→        double dist;
   124→        if (len_sq == 0) {
   125→            dist = std::sqrt(px * px + py * py);
   126→        } else {
   127→            double t = std::max(0.0, std::min(1.0, (px * dx + py * dy) / len_sq));
   128→            double proj_x = t * dx - px, proj_y = t * dy - py;
   129→            dist = std::sqrt(proj_x * proj_x + proj_y * proj_y);
   130→        }
   131→        if (dist > max_dist) {
   132→            max_dist = dist;
   133→            max_idx = i;
   134→        }
   135→    }
   136→
   137→    if (max_dist > epsilon) {
   138→        keep[max_idx] = true;
   139→        dp_simplify(pts, start, max_idx, epsilon, keep);
   140→        dp_simplify(pts, max_idx, end, epsilon, keep);
   141→    }
   142→}
   143→
   144→static constexpr size_t MAX_POLYGON_VERTICES = 500;
   145→
   146→inline std::vector<std::pair<double,double>> simplify_polygon(
   147→    const std::vector<std::pair<double,double>>& pts, size_t max_vertices = MAX_POLYGON_VERTICES) {
   148→    if (pts.size() <= max_vertices) return pts;
   149→
   150→    double lo = 0, hi = 1.0;
   151→    std::vector<std::pair<double,double>> result;
   152→
   153→    for (int iter = 0; iter < 20; iter++) {
   154→        double epsilon = (lo + hi) / 2;
   155→        std::vector<bool> keep(pts.size(), false);
   156→        keep[0] = true;
   157→        keep[pts.size() - 1] = true;
   158→        dp_simplify(pts, 0, pts.size() - 1, epsilon, keep);
   159→
   160→        size_t count = 0;
   161→        for (bool k : keep) if (k) count++;
   162→
   163→        if (count > max_vertices) {
   164→            lo = epsilon;
   165→        } else {
   166→            hi = epsilon;
   167→        }
   168→    }
   169→
   170→    std::vector<bool> keep(pts.size(), false);
   171→    keep[0] = true;
   172→    keep[pts.size() - 1] = true;
   173→    dp_simplify(pts, 0, pts.size() - 1, hi, keep);
   174→
   175→    result.clear();
   176→    for (size_t i = 0; i < pts.size(); i++) {
   177→        if (keep[i]) result.push_back(pts[i]);
   178→    }
   179→    return result;
   180→}
   181→
   182→// Parse leading digits from a house number string
   183→inline uint32_t parse_house_number(const char* s) {
   184→    if (!s) return 0;
   185→    uint32_t n = 0;
   186→    while (*s >= '0' && *s <= '9') {
   187→        n = n * 10 + (*s - '0');
   188→        s++;
   189→    }
   190→    return n;
   191→}
   192→
   193→// Highway types excluded from street indexing
   194→inline bool is_included_highway(const char* value) {
   195→    static const char* kExcluded[] = {
   196→        "footway", "path", "track", "steps", "cycleway",
   197→        "service", "pedestrian", "bridleway", "construction"
   198→    };
   199→    for (const char* excluded : kExcluded) {
   200→        if (std::strcmp(excluded, value) == 0) return false;
   201→    }
   202→    return true;
   203→}
   204→


Now let me read the remaining sections of build_index.cpp.


   345→static std::vector<std::vector<std::pair<double,double>>> assemble_outer_rings(
   346→    const std::vector<std::pair<int64_t, std::string>>& members,
   347→    const std::unordered_map<int64_t, ParsedData::WayGeometry>& way_geoms,
   348→    bool include_all_roles = false)
   349→{
   350→    // Coordinate-based ring assembly: split ways at shared internal nodes,
   351→    // then stitch sub-ways at endpoints with backtracking.
   352→    // This matches osmium's segment-level approach.
   353→    // coord_key() from geometry.h provides coordinate hashing.
   354→
   355→    // Collect coordinates from member ways.
   356→    // First pass uses outer/empty-role ways only. If that produces 0 rings,
   357→    // retry with ALL ways (matching osmium's check_roles=false fallback) —
   358→    // inner ways sometimes bridge gaps between outer ways.
   359→    std::unordered_map<int64_t, int> coord_count;
   360→    std::vector<const std::vector<std::pair<double,double>>*> way_coords;
   361→    for (const auto& [way_id, role] : members) {
   362→        if (!include_all_roles && role != "outer" && !role.empty()) continue;
   363→        auto it = way_geoms.find(way_id);
   364→        if (it == way_geoms.end() || it->second.coords.empty()) continue;
   365→        way_coords.push_back(&it->second.coords);
   366→        for (const auto& [lat, lng] : it->second.coords) {
   367→            coord_count[coord_key(lat, lng)]++;
   368→        }
   369→    }
   370→
   371→    // A coordinate is a "split point" if it appears in 2+ ways' node lists.
   372→    // Endpoints always count, internal nodes that appear in multiple ways need splitting.
   373→    // Also, all way endpoints are natural split points.
   374→    std::unordered_set<int64_t> split_points;
   375→    for (auto* coords : way_coords) {
   376→        split_points.insert(coord_key(coords->front().first, coords->front().second));
   377→        split_points.insert(coord_key(coords->back().first, coords->back().second));
   378→    }
   379→    for (auto& [ck, cnt] : coord_count) {
   380→        if (cnt > 1) split_points.insert(ck);
   381→    }
   382→
   383→    // Split each way at split points to create sub-ways
   384→    struct SubWay {
   385→        std::vector<std::pair<double,double>> coords;
   386→    };
   387→    std::vector<SubWay> sub_ways;
   388→    for (auto* coords : way_coords) {
   389→        size_t seg_start = 0;
   390→        for (size_t i = 1; i < coords->size(); i++) {
   391→            int64_t ck = coord_key((*coords)[i].first, (*coords)[i].second);
   392→            if (split_points.count(ck) && i > seg_start) {
   393→                // Create sub-way from seg_start to i (inclusive)
   394→                SubWay sw;
   395→                sw.coords.assign(coords->begin() + seg_start, coords->begin() + i + 1);
   396→                if (sw.coords.size() >= 2) {
   397→                    sub_ways.push_back(std::move(sw));
   398→                }
   399→                seg_start = i;
   400→            }
   401→        }
   402→        // Handle remaining segment (if not already added)
   403→        if (seg_start < coords->size() - 1) {
   404→            SubWay sw;
   405→            sw.coords.assign(coords->begin() + seg_start, coords->end());
   406→            if (sw.coords.size() >= 2) {
   407→                sub_ways.push_back(std::move(sw));
   408→            }
   409→        }
   410→    }
   411→
   412→    // Build adjacency by sub-way coordinate endpoints
   413→    struct WayRef {
   414→        int64_t way_id;
   415→        const ParsedData::WayGeometry* geom; // unused for sub-ways
   416→    };
   417→    // We'll adapt the BacktrackState to work with sub_ways directly
   418→    std::unordered_map<int64_t, std::vector<std::pair<size_t, bool>>> coord_adj;
   419→    for (size_t i = 0; i < sub_ways.size(); i++) {
   420→        auto& first = sub_ways[i].coords.front();
   421→        auto& last = sub_ways[i].coords.back();
   422→        coord_adj[coord_key(first.first, first.second)].push_back({i, false});
   423→        coord_adj[coord_key(last.first, last.second)].push_back({i, true});
   424→    }
   425→
   426→    std::vector<bool> used(sub_ways.size(), false);
   427→    std::vector<std::vector<std::pair<double,double>>> rings;
   428→
   429→    // Ring closure: greedy first (fast), then full backtracking retry from
   430→    // scratch if greedy left unused sub-ways. The second pass gets clean
   431→    // used flags so backtracking makes optimal branch choices.
   432→
   433→    // Backtracking with depth limit (for fallback only)
   434→    struct BacktrackState {
   435→        const std::vector<SubWay>& sways;
   436→        const std::unordered_map<int64_t, std::vector<std::pair<size_t, bool>>>& adj;
   437→        std::vector<bool>& used;
   438→        int calls;
   439→
   440→        bool try_close(int64_t first_key, int64_t last_key,
   441→                       std::vector<std::pair<size_t, bool>>& path, int depth) {
   442→            if (++calls > BACKTRACK_CALL_BUDGET) return false; // limit total work, not just depth
   443→            if (!path.empty() && first_key == last_key) return true;
   444→            if (depth > BACKTRACK_MAX_DEPTH) return false;
   445→
   446→            auto it = adj.find(last_key);
   447→            if (it == adj.end()) return false;
   448→
   449→            for (const auto& [wi, is_last] : it->second) {
   450→                if (used[wi]) continue;
   451→                auto& endpoint = is_last ? sways[wi].coords.front() : sways[wi].coords.back();
   452→                int64_t new_key = coord_key(endpoint.first, endpoint.second);
   453→
   454→                used[wi] = true;
   455→                path.push_back({wi, is_last});
   456→
   457→                if (try_close(first_key, new_key, path, depth + 1)) return true;
   458→
   459→                path.pop_back();
   460→                used[wi] = false;
   461→            }
   462→            return false;
   463→        }
   464→    };
   465→
   466→    BacktrackState bt{sub_ways, coord_adj, used, 0};
   467→
   468→    // Pass 1: Greedy (fast, O(n) per ring)
   469→    for (size_t start_idx = 0; start_idx < sub_ways.size(); start_idx++) {
   470→        if (used[start_idx]) continue;
   471→
   472→        const auto& sg = sub_ways[start_idx];
   473→        int64_t first_key = coord_key(sg.coords.front().first, sg.coords.front().second);
   474→        int64_t last_key = coord_key(sg.coords.back().first, sg.coords.back().second);
   475→
   476→        if (sg.coords.size() >= 4 && first_key == last_key) {
   477→            used[start_idx] = true;
   478→            rings.push_back(sg.coords);
   479→            continue;
   480→        }
   481→
   482→        used[start_idx] = true;
   483→        std::vector<size_t> attempt_used = {start_idx};
   484→        std::vector<std::pair<double,double>> ring = sg.coords;
   485→        int64_t current_last = last_key;
   486→        bool extended = true;
   487→        while (extended && current_last != first_key) {
   488→            extended = false;
   489→            auto it = coord_adj.find(current_last);
   490→            if (it == coord_adj.end()) break;
   491→            for (const auto& [wi, is_last] : it->second) {
   492→                if (used[wi]) continue;
   493→                const auto& sw = sub_ways[wi];
   494→                bool reversed = is_last;
   495→                auto& endpoint = reversed ? sw.coords.front() : sw.coords.back();
   496→                used[wi] = true;
   497→                attempt_used.push_back(wi);
   498→                if (!reversed) ring.insert(ring.end(), sw.coords.begin() + 1, sw.coords.end());
   499→                else for (auto rit = sw.coords.rbegin() + 1; rit != sw.coords.rend(); ++rit) ring.push_back(*rit);
   500→                current_last = coord_key(endpoint.first, endpoint.second);
   501→                extended = true;
   502→                break;
   503→            }
   504→        }
   505→        if (ring.size() >= 4 && current_last == first_key &&
   506→            !ring_has_self_intersection(ring)) {
   507→            rings.push_back(std::move(ring));
   508→        } else {
   509→            for (auto wi : attempt_used) used[wi] = false;
   510→        }
   511→    }
   512→
   513→    // Pass 2: If greedy left unused sub-ways, retry from scratch with backtracking.
   514→    // Backtracking makes correct branch choices but is slower.
   515→    {
   516→        bool any_unused = false;
   517→        for (size_t i = 0; i < used.size(); i++) {
   518→            if (!used[i]) { any_unused = true; break; }
   519→        }
   520→        if (any_unused && sub_ways.size() <= MAX_SUBWAYS_FOR_RETRY) {
   521→            // Only retry for small relations — large ones stall the build
   522→            std::vector<bool> bt_used(sub_ways.size(), false);
   523→            std::vector<std::vector<std::pair<double,double>>> bt_rings;
   524→
   525→            BacktrackState bt2{sub_ways, coord_adj, bt_used, 0};
   526→            int total_bt_calls = 0;
   527→            constexpr int MAX_TOTAL_BT_CALLS = BACKTRACK_TOTAL_BUDGET;
   528→
   529→            for (size_t si = 0; si < sub_ways.size(); si++) {
   530→                if (bt_used[si]) continue;
   531→                if (total_bt_calls >= MAX_TOTAL_BT_CALLS) break; // budget exceeded
   532→                const auto& sg = sub_ways[si];
   533→                int64_t fk = coord_key(sg.coords.front().first, sg.coords.front().second);
   534→                int64_t lk = coord_key(sg.coords.back().first, sg.coords.back().second);
   535→                if (sg.coords.size() >= 4 && fk == lk) {
   536→                    bt_used[si] = true;
   537→                    bt_rings.push_back(sg.coords);
   538→                    continue;
   539→                }
   540→                bt_used[si] = true;
   541→                bt2.calls = 0;
   542→                std::vector<std::pair<size_t, bool>> path;
   543→                if (bt2.try_close(fk, lk, path, 0)) {
   544→                    std::vector<std::pair<double,double>> r = sg.coords;
   545→                    for (const auto& [wi, rev] : path) {
   546→                        const auto& c = sub_ways[wi].coords;
   547→                        if (!rev) r.insert(r.end(), c.begin() + 1, c.end());
   548→                        else for (auto it2 = c.rbegin() + 1; it2 != c.rend(); ++it2) r.push_back(*it2);
   549→                    }
   550→                    if (r.size() >= 4 && !ring_has_self_intersection(r)) {
   551→                        bt_rings.push_back(std::move(r));
   552→                        continue;
   553→                    }
   554→                }
   555→                bt_used[si] = false;
   556→                total_bt_calls += bt2.calls;
   557→            }
   558→
   559→            // Keep whichever pass produced more rings
   560→            if (bt_rings.size() > rings.size()) {
   561→                rings = std::move(bt_rings);
   562→            }
   563→        }
   564→    }
   565→
   566→    return rings;
   567→}
   568→
   569→// is_included_highway, parse_house_number are in geometry.h
   570→
   571→// --- Add an address point ---
   572→
   573→static void add_addr_point(ParsedData& data, double lat, double lng,
   574→                           const char* housenumber, const char* street,
   575→                           uint64_t& addr_count_total) {
   576→    uint32_t addr_id = static_cast<uint32_t>(data.addr_points.size());
   577→    data.addr_points.push_back({
   578→        static_cast<float>(lat),
   579→        static_cast<float>(lng),
   580→        data.string_pool.intern(housenumber),
   581→        data.string_pool.intern(street)
   582→    });
   583→
   584→    S2CellId cell = point_to_cell(lat, lng);
   585→    data.cell_to_addrs[cell.id()].push_back(addr_id);
   586→
   587→    addr_count_total++;
   588→    if (addr_count_total % 1000000 == 0) {
   589→        std::cerr << "Collected " << addr_count_total / 1000000 << "M addresses..." << std::endl;
   590→    }
   591→}
   592→
   593→// --- Add an admin polygon ---
   594→
   595→static void add_admin_polygon(ParsedData& data,
   596→                               const std::vector<std::pair<double,double>>& vertices_in,
   597→                               const char* name, uint8_t admin_level,
   598→                               const char* country_code,
   599→                               AdminCoverPool* admin_pool = nullptr) {
   600→    // Normalize ring rotation: start at the vertex with smallest (lat, lng)
   601→    // This ensures identical output regardless of ring assembly order.
   602→    auto vertices = vertices_in;
   603→    if (vertices.size() >= 4 &&
   604→        std::fabs(vertices.front().first - vertices.back().first) < 1e-7 &&
   605→        std::fabs(vertices.front().second - vertices.back().second) < 1e-7) {
   606→        vertices.pop_back();
   607→        auto min_it = std::min_element(vertices.begin(), vertices.end());
   608→        std::rotate(vertices.begin(), min_it, vertices.end());
   609→        vertices.push_back(vertices.front());
   610→    }
   611→
   612→    // Simplify large polygons
   613→    auto simplified = simplify_polygon(vertices, 500);
   614→    if (simplified.size() < 3) return;
   615→
   616→    uint32_t poly_id = static_cast<uint32_t>(data.admin_polygons.size());
   617→    uint32_t vertex_offset = static_cast<uint32_t>(data.admin_vertices.size());
   618→
   619→    for (const auto& [lat, lng] : simplified) {
   620→        data.admin_vertices.push_back({static_cast<float>(lat), static_cast<float>(lng)});
   621→    }
   622→
   623→    AdminPolygon poly{};
   624→    poly.vertex_offset = vertex_offset;
   625→    poly.vertex_count = static_cast<uint16_t>(std::min(simplified.size(), size_t(65535)));
   626→    poly.name_id = data.string_pool.intern(name);
   627→    poly.admin_level = admin_level;
   628→    poly.area = polygon_area(simplified);
   629→    poly.country_code = (country_code && country_code[0] && country_code[1])
   630→        ? static_cast<uint16_t>((country_code[0] << 8) | country_code[1])
   631→        : 0;
   632→    data.admin_polygons.push_back(poly);
   633→
   634→    // Submit S2 cell coverage to thread pool (runs concurrently with PBF reading)
   635→    if (admin_pool) {
   636→        admin_pool->submit(poly_id, std::move(simplified));
   637→    }
   638→}
   639→
   640→// --- Relation collector for parallel admin assembly (runs during pass 1) ---
   641→
   642→struct RelationCollector : public osmium::handler::Handler {
   643→    std::vector<CollectedRelation>& relations;
   644→
   645→    explicit RelationCollector(std::vector<CollectedRelation>& r) : relations(r) {}
   646→
   647→    void relation(const osmium::Relation& rel) {
   648→        const char* boundary = rel.tags()["boundary"];
   649→        if (!boundary) return;
   650→
   651→        bool is_admin = (std::strcmp(boundary, "administrative") == 0);
   652→        bool is_postal = (std::strcmp(boundary, "postal_code") == 0);
   653→        if (!is_admin && !is_postal) return;
   654→
   655→        uint8_t admin_level = 0;
   656→        if (is_admin) {
   657→            const char* level_str = rel.tags()["admin_level"];
   658→            if (!level_str) return;
   659→            admin_level = static_cast<uint8_t>(std::atoi(level_str));
   660→            if (admin_level < 2 || admin_level > 10) return;
   661→        } else {
   662→            admin_level = 11; // use 11 for postal codes
   663→        }
   664→        if (kMaxAdminLevel > 0 && admin_level > kMaxAdminLevel) return;
   665→
   666→        const char* name = rel.tags()["name"];
   667→        if (!name && is_admin) return;
   668→
   669→        // For postal codes, use postal_code tag as name
   670→        std::string name_str;
   671→        if (is_postal) {
   672→            const char* postal_code = rel.tags()["postal_code"];
   673→            if (!postal_code) postal_code = name;
   674→            if (!postal_code) return;
   675→            name_str = postal_code;
   676→        } else {
   677→            name_str = name;
   678→        }
   679→
   680→        // Extract country code for level 2 boundaries
   681→        std::string country_code;
   682→        if (admin_level == 2) {
   683→            const char* cc = rel.tags()["ISO3166-1:alpha2"];
   684→            if (cc) country_code = cc;
   685→        }
   686→
   687→        // Collect member way IDs and roles
   688→        CollectedRelation cr;
   689→        cr.id = rel.id();
   690→        cr.admin_level = admin_level;
   691→        cr.name = std::move(name_str);
   692→        cr.country_code = std::move(country_code);
   693→        cr.is_postal = is_postal;
   694→


   695→        for (const auto& member : rel.members()) {
   696→            if (member.type() == osmium::item_type::way) {
   697→                cr.members.emplace_back(member.ref(), member.role());
   698→            }
   699→        }
   700→
   701→        if (!cr.members.empty()) {
   702→            relations.push_back(std::move(cr));
   703→        }
   704→    }
   705→};
   706→
   707→// --- Resolve interpolation way endpoint house numbers ---
   708→
   709→static void resolve_interpolation_endpoints(ParsedData& data) {
   710→    struct CoordKey {
   711→        int32_t lat;
   712→        int32_t lng;
   713→        bool operator==(const CoordKey& o) const { return lat == o.lat && lng == o.lng; }
   714→    };
   715→    struct CoordHash {
   716→        size_t operator()(const CoordKey& k) const {
   717→            return std::hash<int64_t>()(((int64_t)k.lat << 32) | (uint32_t)k.lng);
   718→        }
   719→    };
   720→
   721→    std::unordered_map<CoordKey, uint32_t, CoordHash> addr_by_coord;
   722→    for (uint32_t i = 0; i < data.addr_points.size(); i++) {
   723→        CoordKey key{
   724→            static_cast<int32_t>(data.addr_points[i].lat * 100000),
   725→            static_cast<int32_t>(data.addr_points[i].lng * 100000)
   726→        };
   727→        addr_by_coord[key] = i;
   728→    }
   729→
   730→    uint32_t resolved = 0;
   731→    for (auto& iw : data.interp_ways) {
   732→        if (iw.node_count < 2) continue;
   733→
   734→        const auto& start = data.interp_nodes[iw.node_offset];
   735→        CoordKey start_key{
   736→            static_cast<int32_t>(start.lat * 100000),
   737→            static_cast<int32_t>(start.lng * 100000)
   738→        };
   739→        auto it_start = addr_by_coord.find(start_key);
   740→
   741→        const auto& end = data.interp_nodes[iw.node_offset + iw.node_count - 1];
   742→        CoordKey end_key{
   743→            static_cast<int32_t>(end.lat * 100000),
   744→            static_cast<int32_t>(end.lng * 100000)
   745→        };
   746→        auto it_end = addr_by_coord.find(end_key);
   747→
   748→        if (it_start != addr_by_coord.end()) {
   749→            const char* hn = data.string_pool.data().data() + data.addr_points[it_start->second].housenumber_id;
   750→            iw.start_number = parse_house_number(hn);
   751→        }
   752→        if (it_end != addr_by_coord.end()) {
   753→            const char* hn = data.string_pool.data().data() + data.addr_points[it_end->second].housenumber_id;
   754→            iw.end_number = parse_house_number(hn);
   755→        }
   756→
   757→        if (iw.start_number > 0 && iw.end_number > 0) resolved++;
   758→    }
   759→
   760→    std::cerr << "Resolved " << resolved << "/" << data.interp_ways.size()
   761→              << " interpolation ways" << std::endl;
   762→}
   763→
   764→// --- Deduplicate IDs per cell ---
   765→
   766→template<typename Map>
   767→static void deduplicate(Map& cell_map) {
   768→    for (auto& [cell_id, ids] : cell_map) {
   769→        std::sort(ids.begin(), ids.end());
   770→        ids.erase(std::unique(ids.begin(), ids.end()), ids.end());
   771→    }
   772→}
   773→
   774→// --- Cache serialization ---
   775→
   776→enum class SectionType : uint32_t {
   777→    STRING_POOL = 0,
   778→    WAYS = 1,
   779→    STREET_NODES = 2,
   780→    CELL_TO_WAYS = 3,
   781→    ADDR_POINTS = 4,
   782→    CELL_TO_ADDRS = 5,
   783→    INTERP_WAYS = 6,
   784→    INTERP_NODES = 7,
   785→    CELL_TO_INTERPS = 8,
   786→    ADMIN_POLYGONS = 9,
   787→    ADMIN_VERTICES = 10,
   788→    CELL_TO_ADMIN = 11,
   789→};
   790→
   791→static const char CACHE_MAGIC[8] = {'T','G','C','A','C','H','E','\0'};
   792→static const uint32_t CACHE_VERSION = 1;
   793→static const uint32_t CACHE_SECTION_COUNT = 12;
   794→
   795→// Serialize a vector of structs to a binary blob
   796→template<typename T>
   797→static std::vector<char> serialize_vector(const std::vector<T>& vec) {
   798→    std::vector<char> buf;
   799→    uint64_t count = vec.size();
   800→    buf.resize(sizeof(uint64_t) + count * sizeof(T));
   801→    std::memcpy(buf.data(), &count, sizeof(uint64_t));
   802→    if (count > 0) {
   803→        std::memcpy(buf.data() + sizeof(uint64_t), vec.data(), count * sizeof(T));
   804→    }
   805→    return buf;
   806→}
   807→
   808→// Deserialize a vector of structs from a binary blob
   809→template<typename T>
   810→static bool deserialize_vector(const char* data, uint64_t length, std::vector<T>& vec) {
   811→    if (length < sizeof(uint64_t)) return false;
   812→    uint64_t count;
   813→    std::memcpy(&count, data, sizeof(uint64_t));
   814→    if (length < sizeof(uint64_t) + count * sizeof(T)) return false;
   815→    vec.resize(count);
   816→    if (count > 0) {
   817→        std::memcpy(vec.data(), data + sizeof(uint64_t), count * sizeof(T));
   818→    }
   819→    return true;
   820→}
   821→
   822→// Serialize a cell map (uint64_t -> vector<uint32_t>)
   823→static std::vector<char> serialize_cell_map(const std::unordered_map<uint64_t, std::vector<uint32_t>>& map) {
   824→    // Calculate total size
   825→    size_t total = sizeof(uint64_t); // entry count
   826→    for (const auto& [cell_id, ids] : map) {
   827→        total += sizeof(uint64_t) + sizeof(uint32_t) + ids.size() * sizeof(uint32_t);
   828→    }
   829→    std::vector<char> buf(total);
   830→    char* ptr = buf.data();
   831→
   832→    uint64_t entry_count = map.size();
   833→    std::memcpy(ptr, &entry_count, sizeof(uint64_t));
   834→    ptr += sizeof(uint64_t);
   835→
   836→    for (const auto& [cell_id, ids] : map) {
   837→        std::memcpy(ptr, &cell_id, sizeof(uint64_t));
   838→        ptr += sizeof(uint64_t);
   839→        uint32_t id_count = static_cast<uint32_t>(ids.size());
   840→        std::memcpy(ptr, &id_count, sizeof(uint32_t));
   841→        ptr += sizeof(uint32_t);
   842→        if (id_count > 0) {
   843→            std::memcpy(ptr, ids.data(), id_count * sizeof(uint32_t));
   844→            ptr += id_count * sizeof(uint32_t);
   845→        }
   846→    }
   847→    return buf;
   848→}
   849→
   850→// Deserialize a cell map
   851→static bool deserialize_cell_map(const char* data, uint64_t length,
   852→                                  std::unordered_map<uint64_t, std::vector<uint32_t>>& map) {
   853→    if (length < sizeof(uint64_t)) return false;
   854→    const char* ptr = data;
   855→    const char* end = data + length;
   856→
   857→    uint64_t entry_count;
   858→    std::memcpy(&entry_count, ptr, sizeof(uint64_t));
   859→    ptr += sizeof(uint64_t);
   860→
   861→    map.reserve(entry_count);
   862→    for (uint64_t i = 0; i < entry_count; i++) {
   863→        if (ptr + sizeof(uint64_t) + sizeof(uint32_t) > end) return false;
   864→        uint64_t cell_id;
   865→        std::memcpy(&cell_id, ptr, sizeof(uint64_t));
   866→        ptr += sizeof(uint64_t);
   867→        uint32_t id_count;
   868→        std::memcpy(&id_count, ptr, sizeof(uint32_t));
   869→        ptr += sizeof(uint32_t);
   870→        if (ptr + id_count * sizeof(uint32_t) > end) return false;
   871→        std::vector<uint32_t> ids(id_count);
   872→        if (id_count > 0) {
   873→            std::memcpy(ids.data(), ptr, id_count * sizeof(uint32_t));
   874→            ptr += id_count * sizeof(uint32_t);
   875→        }
   876→        map[cell_id] = std::move(ids);
   877→    }
   878→    return true;
   879→}
   880→
   881→static void serialize_cache(const ParsedData& data, const std::string& path) {
   882→    std::cerr << "Saving cache to " << path << "..." << std::endl;
   883→    std::ofstream f(path, std::ios::binary);
   884→    if (!f.is_open()) {
   885→        std::cerr << "Error: could not open cache file for writing: " << path << std::endl;
   886→        return;
   887→    }
   888→
   889→    f.write(CACHE_MAGIC, 8);
   890→    f.write(reinterpret_cast<const char*>(&CACHE_VERSION), sizeof(uint32_t));
   891→    f.write(reinterpret_cast<const char*>(&CACHE_SECTION_COUNT), sizeof(uint32_t));
   892→
   893→    // Helper to write a section
   894→    auto write_section = [&](SectionType type, const std::vector<char>& blob) {
   895→        uint32_t t = static_cast<uint32_t>(type);
   896→        uint64_t len = blob.size();
   897→        f.write(reinterpret_cast<const char*>(&t), sizeof(uint32_t));
   898→        f.write(reinterpret_cast<const char*>(&len), sizeof(uint64_t));
   899→        f.write(blob.data(), len);
   900→    };
   901→
   902→    // STRING_POOL: raw char vector
   903→    {
   904→        const auto& sp = data.string_pool.data();
   905→        std::vector<char> blob(sizeof(uint64_t) + sp.size());
   906→        uint64_t sz = sp.size();
   907→        std::memcpy(blob.data(), &sz, sizeof(uint64_t));
   908→        if (sz > 0) std::memcpy(blob.data() + sizeof(uint64_t), sp.data(), sz);
   909→        write_section(SectionType::STRING_POOL, blob);
   910→    }
   911→
   912→    write_section(SectionType::WAYS, serialize_vector(data.ways));
   913→    write_section(SectionType::STREET_NODES, serialize_vector(data.street_nodes));
   914→    write_section(SectionType::CELL_TO_WAYS, serialize_cell_map(data.cell_to_ways));
   915→    write_section(SectionType::ADDR_POINTS, serialize_vector(data.addr_points));
   916→    write_section(SectionType::CELL_TO_ADDRS, serialize_cell_map(data.cell_to_addrs));
   917→    write_section(SectionType::INTERP_WAYS, serialize_vector(data.interp_ways));
   918→    write_section(SectionType::INTERP_NODES, serialize_vector(data.interp_nodes));
   919→    write_section(SectionType::CELL_TO_INTERPS, serialize_cell_map(data.cell_to_interps));
   920→    write_section(SectionType::ADMIN_POLYGONS, serialize_vector(data.admin_polygons));
   921→    write_section(SectionType::ADMIN_VERTICES, serialize_vector(data.admin_vertices));
   922→    write_section(SectionType::CELL_TO_ADMIN, serialize_cell_map(data.cell_to_admin));
   923→
   924→    std::cerr << "Cache saved." << std::endl;
   925→}
   926→
   927→static bool deserialize_cache(ParsedData& data, const std::string& path) {
   928→    std::cerr << "Loading cache from " << path << "..." << std::endl;
   929→    std::ifstream f(path, std::ios::binary);
   930→    if (!f.is_open()) {
   931→        std::cerr << "Error: could not open cache file: " << path << std::endl;
   932→        return false;
   933→    }
   934→
   935→    char magic[8];
   936→    f.read(magic, 8);
   937→    if (std::memcmp(magic, CACHE_MAGIC, 8) != 0) {
   938→        std::cerr << "Error: invalid cache magic" << std::endl;
   939→        return false;
   940→    }
   941→
   942→    uint32_t version;
   943→    f.read(reinterpret_cast<char*>(&version), sizeof(uint32_t));
   944→    if (version != CACHE_VERSION) {
   945→        std::cerr << "Error: unsupported cache version " << version << std::endl;
   946→        return false;
   947→    }
   948→
   949→    uint32_t section_count;
   950→    f.read(reinterpret_cast<char*>(&section_count), sizeof(uint32_t));
   951→
   952→    for (uint32_t s = 0; s < section_count; s++) {
   953→        uint32_t type;
   954→        uint64_t length;
   955→        f.read(reinterpret_cast<char*>(&type), sizeof(uint32_t));
   956→        f.read(reinterpret_cast<char*>(&length), sizeof(uint64_t));
   957→
   958→        std::vector<char> blob(length);
   959→        f.read(blob.data(), length);
   960→
   961→        switch (static_cast<SectionType>(type)) {
   962→        case SectionType::STRING_POOL: {
   963→            if (length < sizeof(uint64_t)) return false;
   964→            uint64_t sz;
   965→            std::memcpy(&sz, blob.data(), sizeof(uint64_t));
   966→            auto& sp = data.string_pool.mutable_data();
   967→            sp.resize(sz);
   968→            if (sz > 0) std::memcpy(sp.data(), blob.data() + sizeof(uint64_t), sz);
   969→            break;
   970→        }
   971→        case SectionType::WAYS:
   972→            if (!deserialize_vector(blob.data(), length, data.ways)) return false;
   973→            break;
   974→        case SectionType::STREET_NODES:
   975→            if (!deserialize_vector(blob.data(), length, data.street_nodes)) return false;
   976→            break;
   977→        case SectionType::CELL_TO_WAYS:
   978→            if (!deserialize_cell_map(blob.data(), length, data.cell_to_ways)) return false;
   979→            break;
   980→        case SectionType::ADDR_POINTS:
   981→            if (!deserialize_vector(blob.data(), length, data.addr_points)) return false;
   982→            break;
   983→        case SectionType::CELL_TO_ADDRS:
   984→            if (!deserialize_cell_map(blob.data(), length, data.cell_to_addrs)) return false;
   985→            break;
   986→        case SectionType::INTERP_WAYS:
   987→            if (!deserialize_vector(blob.data(), length, data.interp_ways)) return false;
   988→            break;
   989→        case SectionType::INTERP_NODES:
   990→            if (!deserialize_vector(blob.data(), length, data.interp_nodes)) return false;
   991→            break;
   992→        case SectionType::CELL_TO_INTERPS:
   993→            if (!deserialize_cell_map(blob.data(), length, data.cell_to_interps)) return false;
   994→            break;
   995→        case SectionType::ADMIN_POLYGONS:
   996→            if (!deserialize_vector(blob.data(), length, data.admin_polygons)) return false;
   997→            break;
   998→        case SectionType::ADMIN_VERTICES:
   999→            if (!deserialize_vector(blob.data(), length, data.admin_vertices)) return false;
  1000→            break;
  1001→        case SectionType::CELL_TO_ADMIN:
  1002→            if (!deserialize_cell_map(blob.data(), length, data.cell_to_admin)) return false;
  1003→            break;
  1004→        default:
  1005→            std::cerr << "Warning: unknown cache section type " << type << ", skipping" << std::endl;
  1006→            break;
  1007→        }
  1008→    }
  1009→
  1010→    std::cerr << "Cache loaded: " << data.ways.size() << " ways, "
  1011→              << data.addr_points.size() << " addrs, "
  1012→              << data.interp_ways.size() << " interps, "
  1013→              << data.admin_polygons.size() << " admin polygons" << std::endl;
  1014→    return true;
  1015→}
  1016→
  1017→// --- Continent bounding box filtering ---
  1018→
  1019→struct ContinentBBox {
  1020→    const char* name;
  1021→    double min_lat, max_lat, min_lng, max_lng;
  1022→};
  1023→
  1024→static const ContinentBBox kContinents[] = {
  1025→    {"africa",            -35.0,  37.5,  -25.0,  55.0},
  1026→    {"asia",              -12.0,  82.0,   25.0, 180.0},
  1027→    {"europe",             35.0,  72.0,  -25.0,  45.0},
  1028→    {"north-america",       7.0,  84.0, -170.0, -50.0},
  1029→    {"south-america",     -56.0,  13.0,  -82.0, -34.0},
  1030→    {"oceania",           -50.0,   0.0,  110.0, 180.0},
  1031→    {"central-america",     7.0,  23.5, -120.0, -57.0},
  1032→    {"antarctica",        -90.0, -60.0, -180.0, 180.0},
  1033→};
  1034→
  1035→static bool cell_in_bbox(uint64_t cell_id, const ContinentBBox& bbox) {
  1036→    S2CellId cell(cell_id);
  1037→    S2LatLng center = cell.ToLatLng();
  1038→    double lat = center.lat().degrees();
  1039→    double lng = center.lng().degrees();
  1040→    return lat >= bbox.min_lat && lat <= bbox.max_lat &&
  1041→           lng >= bbox.min_lng && lng <= bbox.max_lng;
  1042→}
  1043→
  1044→static ParsedData filter_by_bbox(const ParsedData& full, const ContinentBBox& bbox) {


  1044→static ParsedData filter_by_bbox(const ParsedData& full, const ContinentBBox& bbox) {
  1045→    ParsedData out;
  1046→
  1047→    // --- Filter cell_to_ways and collect referenced way IDs ---
  1048→    std::unordered_set<uint32_t> used_way_ids;
  1049→    for (const auto& [cell_id, ids] : full.cell_to_ways) {
  1050→        if (cell_in_bbox(cell_id, bbox)) {
  1051→            for (uint32_t id : ids) {
  1052→                used_way_ids.insert(id);
  1053→            }
  1054→        }
  1055→    }
  1056→
  1057→    // --- Filter cell_to_addrs and collect referenced addr IDs ---
  1058→    std::unordered_set<uint32_t> used_addr_ids;
  1059→    for (const auto& [cell_id, ids] : full.cell_to_addrs) {
  1060→        if (cell_in_bbox(cell_id, bbox)) {
  1061→            for (uint32_t id : ids) {
  1062→                used_addr_ids.insert(id);
  1063→            }
  1064→        }
  1065→    }
  1066→
  1067→    // --- Filter cell_to_interps and collect referenced interp IDs ---
  1068→    std::unordered_set<uint32_t> used_interp_ids;
  1069→    for (const auto& [cell_id, ids] : full.cell_to_interps) {
  1070→        if (cell_in_bbox(cell_id, bbox)) {
  1071→            for (uint32_t id : ids) {
  1072→                used_interp_ids.insert(id);
  1073→            }
  1074→        }
  1075→    }
  1076→
  1077→    // --- Filter cell_to_admin and collect referenced admin IDs ---
  1078→    std::unordered_set<uint32_t> used_admin_ids;
  1079→    for (const auto& [cell_id, ids] : full.cell_to_admin) {
  1080→        if (cell_in_bbox(cell_id, bbox)) {
  1081→            for (uint32_t id : ids) {
  1082→                used_admin_ids.insert(id & ID_MASK);
  1083→            }
  1084→        }
  1085→    }
  1086→
  1087→    // --- Build remapped ways ---
  1088→    std::unordered_map<uint32_t, uint32_t> way_remap; // old_id -> new_id
  1089→    {
  1090→        std::vector<uint32_t> sorted_ids(used_way_ids.begin(), used_way_ids.end());
  1091→        std::sort(sorted_ids.begin(), sorted_ids.end());
  1092→        for (uint32_t old_id : sorted_ids) {
  1093→            uint32_t new_id = static_cast<uint32_t>(out.ways.size());
  1094→            way_remap[old_id] = new_id;
  1095→            const auto& w = full.ways[old_id];
  1096→            WayHeader nw = w;
  1097→            nw.node_offset = static_cast<uint32_t>(out.street_nodes.size());
  1098→            out.ways.push_back(nw);
  1099→            for (uint8_t n = 0; n < w.node_count; n++) {
  1100→                out.street_nodes.push_back(full.street_nodes[w.node_offset + n]);
  1101→            }
  1102→        }
  1103→    }
  1104→
  1105→    // --- Build remapped addr points ---
  1106→    std::unordered_map<uint32_t, uint32_t> addr_remap;
  1107→    {
  1108→        std::vector<uint32_t> sorted_ids(used_addr_ids.begin(), used_addr_ids.end());
  1109→        std::sort(sorted_ids.begin(), sorted_ids.end());
  1110→        for (uint32_t old_id : sorted_ids) {
  1111→            uint32_t new_id = static_cast<uint32_t>(out.addr_points.size());
  1112→            addr_remap[old_id] = new_id;
  1113→            out.addr_points.push_back(full.addr_points[old_id]);
  1114→        }
  1115→    }
  1116→
  1117→    // --- Build remapped interp ways ---
  1118→    std::unordered_map<uint32_t, uint32_t> interp_remap;
  1119→    {
  1120→        std::vector<uint32_t> sorted_ids(used_interp_ids.begin(), used_interp_ids.end());
  1121→        std::sort(sorted_ids.begin(), sorted_ids.end());
  1122→        for (uint32_t old_id : sorted_ids) {
  1123→            uint32_t new_id = static_cast<uint32_t>(out.interp_ways.size());
  1124→            interp_remap[old_id] = new_id;
  1125→            const auto& iw = full.interp_ways[old_id];
  1126→            InterpWay niw = iw;
  1127→            niw.node_offset = static_cast<uint32_t>(out.interp_nodes.size());
  1128→            out.interp_ways.push_back(niw);
  1129→            for (uint8_t n = 0; n < iw.node_count; n++) {
  1130→                out.interp_nodes.push_back(full.interp_nodes[iw.node_offset + n]);
  1131→            }
  1132→        }
  1133→    }
  1134→
  1135→    // --- Build remapped admin polygons ---
  1136→    std::unordered_map<uint32_t, uint32_t> admin_remap;
  1137→    {
  1138→        std::vector<uint32_t> sorted_ids(used_admin_ids.begin(), used_admin_ids.end());
  1139→        std::sort(sorted_ids.begin(), sorted_ids.end());
  1140→        for (uint32_t old_id : sorted_ids) {
  1141→            uint32_t new_id = static_cast<uint32_t>(out.admin_polygons.size());
  1142→            admin_remap[old_id] = new_id;
  1143→            const auto& ap = full.admin_polygons[old_id];
  1144→            AdminPolygon nap = ap;
  1145→            nap.vertex_offset = static_cast<uint32_t>(out.admin_vertices.size());
  1146→            out.admin_polygons.push_back(nap);
  1147→            for (uint16_t v = 0; v < ap.vertex_count; v++) {
  1148→                out.admin_vertices.push_back(full.admin_vertices[ap.vertex_offset + v]);
  1149→            }
  1150→        }
  1151→    }
  1152→
  1153→    // --- Build remapped cell maps ---
  1154→    for (const auto& [cell_id, ids] : full.cell_to_ways) {
  1155→        if (!cell_in_bbox(cell_id, bbox)) continue;
  1156→        std::vector<uint32_t> new_ids;
  1157→        for (uint32_t id : ids) {
  1158→            auto it = way_remap.find(id);
  1159→            if (it != way_remap.end()) {
  1160→                new_ids.push_back(it->second);
  1161→            }
  1162→        }
  1163→        if (!new_ids.empty()) {
  1164→            out.cell_to_ways[cell_id] = std::move(new_ids);
  1165→        }
  1166→    }
  1167→
  1168→    for (const auto& [cell_id, ids] : full.cell_to_addrs) {
  1169→        if (!cell_in_bbox(cell_id, bbox)) continue;
  1170→        std::vector<uint32_t> new_ids;
  1171→        for (uint32_t id : ids) {
  1172→            auto it = addr_remap.find(id);
  1173→            if (it != addr_remap.end()) {
  1174→                new_ids.push_back(it->second);
  1175→            }
  1176→        }
  1177→        if (!new_ids.empty()) {
  1178→            out.cell_to_addrs[cell_id] = std::move(new_ids);
  1179→        }
  1180→    }
  1181→
  1182→    for (const auto& [cell_id, ids] : full.cell_to_interps) {
  1183→        if (!cell_in_bbox(cell_id, bbox)) continue;
  1184→        std::vector<uint32_t> new_ids;
  1185→        for (uint32_t id : ids) {
  1186→            auto it = interp_remap.find(id);
  1187→            if (it != interp_remap.end()) {
  1188→                new_ids.push_back(it->second);
  1189→            }
  1190→        }
  1191→        if (!new_ids.empty()) {
  1192→            out.cell_to_interps[cell_id] = std::move(new_ids);
  1193→        }
  1194→    }
  1195→
  1196→    for (const auto& [cell_id, ids] : full.cell_to_admin) {
  1197→        if (!cell_in_bbox(cell_id, bbox)) continue;
  1198→        std::vector<uint32_t> new_ids;
  1199→        for (uint32_t id : ids) {
  1200→            uint32_t raw_id = id & ID_MASK;
  1201→            uint32_t flags = id & INTERIOR_FLAG;
  1202→            auto it = admin_remap.find(raw_id);
  1203→            if (it != admin_remap.end()) {
  1204→                new_ids.push_back(it->second | flags);
  1205→            }
  1206→        }
  1207→        if (!new_ids.empty()) {
  1208→            out.cell_to_admin[cell_id] = std::move(new_ids);
  1209→        }
  1210→    }
  1211→
  1212→    // --- Rebuild compact string pool with only referenced strings ---
  1213→    // Collect all referenced string offsets
  1214→    std::unordered_set<uint32_t> used_offsets;
  1215→    for (const auto& w : out.ways) {
  1216→        used_offsets.insert(w.name_id);
  1217→    }
  1218→    for (const auto& a : out.addr_points) {
  1219→        used_offsets.insert(a.housenumber_id);
  1220→        used_offsets.insert(a.street_id);
  1221→    }
  1222→    for (const auto& iw : out.interp_ways) {
  1223→        used_offsets.insert(iw.street_id);
  1224→    }
  1225→    for (const auto& ap : out.admin_polygons) {
  1226→        used_offsets.insert(ap.name_id);
  1227→    }
  1228→
  1229→    // Build remap: old offset -> new offset
  1230→    const auto& old_sp = full.string_pool.data();
  1231→    std::unordered_map<uint32_t, uint32_t> string_remap;
  1232→    auto& new_sp = out.string_pool.mutable_data();
  1233→    new_sp.clear();
  1234→
  1235→    std::vector<uint32_t> sorted_offsets(used_offsets.begin(), used_offsets.end());
  1236→    std::sort(sorted_offsets.begin(), sorted_offsets.end());
  1237→
  1238→    for (uint32_t old_off : sorted_offsets) {
  1239→        uint32_t new_off = static_cast<uint32_t>(new_sp.size());
  1240→        string_remap[old_off] = new_off;
  1241→        // Copy the null-terminated string
  1242→        const char* str = old_sp.data() + old_off;
  1243→        size_t len = std::strlen(str);
  1244→        new_sp.insert(new_sp.end(), str, str + len + 1);
  1245→    }
  1246→
  1247→    // Remap all string IDs in the output data
  1248→    for (auto& w : out.ways) {
  1249→        w.name_id = string_remap[w.name_id];
  1250→    }
  1251→    for (auto& a : out.addr_points) {
  1252→        a.housenumber_id = string_remap[a.housenumber_id];
  1253→        a.street_id = string_remap[a.street_id];
  1254→    }
  1255→    for (auto& iw : out.interp_ways) {
  1256→        iw.street_id = string_remap[iw.street_id];
  1257→    }
  1258→    for (auto& ap : out.admin_polygons) {
  1259→        ap.name_id = string_remap[ap.name_id];
  1260→    }
  1261→
  1262→    return out;
  1263→}
  1264→
  1265→// --- Write cell index ---
  1266→
  1267→// NO_DATA is in types.h
  1268→
  1269→// Write entries file from a hash map (used for addr and interp cell maps).
  1270→static std::vector<uint32_t> write_entries(
  1271→    const std::string& path,
  1272→    const std::vector<uint64_t>& sorted_cells,
  1273→    const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map
  1274→) {
  1275→    struct CellRef { uint64_t cell_id; const std::vector<uint32_t>* ids; };
  1276→    std::vector<CellRef> sorted_refs;
  1277→    sorted_refs.reserve(cell_map.size());
  1278→    for (auto& [id, ids] : cell_map) {
  1279→        sorted_refs.push_back({id, &ids});
  1280→    }
  1281→    std::sort(sorted_refs.begin(), sorted_refs.end(),
  1282→        [](const CellRef& a, const CellRef& b) { return a.cell_id < b.cell_id; });
  1283→
  1284→    std::vector<uint32_t> offsets(sorted_cells.size(), NO_DATA);
  1285→    size_t total_size = 0;
  1286→    for (auto& r : sorted_refs) total_size += sizeof(uint16_t) + r.ids->size() * sizeof(uint32_t);
  1287→
  1288→    std::vector<char> buf;
  1289→    buf.reserve(total_size);
  1290→    uint32_t current = 0;
  1291→    size_t ri = 0;
  1292→    for (uint32_t si = 0; si < sorted_cells.size() && ri < sorted_refs.size(); si++) {
  1293→        if (sorted_cells[si] < sorted_refs[ri].cell_id) continue;
  1294→        if (sorted_cells[si] > sorted_refs[ri].cell_id) { si--; ri++; continue; }
  1295→        offsets[si] = current;
  1296→        const auto& ids = *sorted_refs[ri].ids;
  1297→        uint16_t count = static_cast<uint16_t>(std::min(ids.size(), size_t(65535)));
  1298→        buf.insert(buf.end(), reinterpret_cast<const char*>(&count),
  1299→                   reinterpret_cast<const char*>(&count) + sizeof(count));
  1300→        buf.insert(buf.end(), reinterpret_cast<const char*>(ids.data()),
  1301→                   reinterpret_cast<const char*>(ids.data()) + ids.size() * sizeof(uint32_t));
  1302→        current += sizeof(uint16_t) + ids.size() * sizeof(uint32_t);
  1303→        ri++;
  1304→    }
  1305→    std::ofstream f(path, std::ios::binary);
  1306→    f.write(buf.data(), buf.size());
  1307→    return offsets;
  1308→}
  1309→
  1310→// Write entries file from sorted pairs — parallel chunked merge-join.
  1311→// Splits sorted_cells into chunks, each thread builds its chunk independently,
  1312→// then prefix-sum computes global offsets and buffers are concatenated.
  1313→static std::vector<uint32_t> write_entries_from_sorted(
  1314→    const std::string& path,
  1315→    const std::vector<uint64_t>& sorted_cells,
  1316→    const std::vector<CellItemPair>& sorted_pairs
  1317→) {
  1318→    std::vector<uint32_t> offsets(sorted_cells.size(), NO_DATA);
  1319→    if (sorted_pairs.empty()) {
  1320→        std::ofstream f(path, std::ios::binary);
  1321→        return offsets;
  1322→    }
  1323→
  1324→    // Find where each chunk of sorted_cells starts in sorted_pairs
  1325→    // using binary search (both are sorted by cell_id)
  1326→    unsigned int nthreads = std::min(std::thread::hardware_concurrency(), 32u);
  1327→    if (nthreads == 0) nthreads = 4;
  1328→    size_t cells_per_chunk = (sorted_cells.size() + nthreads - 1) / nthreads;
  1329→
  1330→    struct ChunkResult {
  1331→        std::vector<char> buf;
  1332→        size_t cell_start, cell_end; // range in sorted_cells
  1333→        uint32_t local_size; // total bytes in this chunk's buffer
  1334→    };
  1335→    std::vector<ChunkResult> chunks(nthreads);
  1336→
  1337→    // Parallel: each thread processes its range of sorted_cells
  1338→    std::vector<std::thread> threads;
  1339→    for (unsigned int t = 0; t < nthreads; t++) {
  1340→        size_t cs = t * cells_per_chunk;
  1341→        size_t ce = std::min(cs + cells_per_chunk, sorted_cells.size());
  1342→        if (cs >= sorted_cells.size()) break;
  1343→


  1344→        threads.emplace_back([&, t, cs, ce]() {
  1345→            auto& chunk = chunks[t];
  1346→            chunk.cell_start = cs;
  1347→            chunk.cell_end = ce;
  1348→            chunk.local_size = 0;
  1349→
  1350→            // Binary search for where this chunk's first cell appears in sorted_pairs
  1351→            size_t pi = std::lower_bound(sorted_pairs.begin(), sorted_pairs.end(),
  1352→                sorted_cells[cs], [](const CellItemPair& p, uint64_t id) {
  1353→                    return p.cell_id < id;
  1354→                }) - sorted_pairs.begin();
  1355→
  1356→            // Merge-join for this chunk
  1357→            for (size_t si = cs; si < ce && pi < sorted_pairs.size(); si++) {
  1358→                if (sorted_cells[si] < sorted_pairs[pi].cell_id) continue;
  1359→                while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id < sorted_cells[si]) pi++;
  1360→                if (pi >= sorted_pairs.size() || sorted_pairs[pi].cell_id != sorted_cells[si]) continue;
  1361→
  1362→                offsets[si] = chunk.local_size; // local offset, adjusted later
  1363→                size_t start = pi;
  1364→                while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id == sorted_cells[si]) pi++;
  1365→                uint16_t count = static_cast<uint16_t>(std::min(pi - start, size_t(65535)));
  1366→                size_t entry_size = sizeof(uint16_t) + (pi - start) * sizeof(uint32_t);
  1367→                size_t buf_pos = chunk.buf.size();
  1368→                chunk.buf.resize(buf_pos + entry_size);
  1369→                memcpy(chunk.buf.data() + buf_pos, &count, sizeof(count));
  1370→                for (size_t k = start; k < pi; k++) {
  1371→                    memcpy(chunk.buf.data() + buf_pos + sizeof(uint16_t) + (k - start) * sizeof(uint32_t),
  1372→                           &sorted_pairs[k].item_id, sizeof(uint32_t));
  1373→                }
  1374→                chunk.local_size += entry_size;
  1375→            }
  1376→        });
  1377→    }
  1378→    for (auto& t : threads) t.join();
  1379→
  1380→    // Prefix-sum: compute global offsets for each chunk
  1381→    uint32_t global_offset = 0;
  1382→    for (auto& chunk : chunks) {
  1383→        for (size_t si = chunk.cell_start; si < chunk.cell_end; si++) {
  1384→            if (offsets[si] != NO_DATA) {
  1385→                offsets[si] += global_offset;
  1386→            }
  1387→        }
  1388→        global_offset += chunk.local_size;
  1389→    }
  1390→
  1391→    // Concatenate buffers and write
  1392→    std::vector<char> buf;
  1393→    buf.reserve(global_offset);
  1394→    for (auto& chunk : chunks) {
  1395→        buf.insert(buf.end(), chunk.buf.begin(), chunk.buf.end());
  1396→    }
  1397→
  1398→    std::ofstream f(path, std::ios::binary);
  1399→    f.write(buf.data(), buf.size());
  1400→    return offsets;
  1401→}
  1402→
  1403→static void write_cell_index(
  1404→    const std::string& cells_path,
  1405→    const std::string& entries_path,
  1406→    const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map
  1407→) {
  1408→    std::vector<std::pair<uint64_t, std::vector<uint32_t>>> sorted(
  1409→        cell_map.begin(), cell_map.end());
  1410→    std::sort(sorted.begin(), sorted.end());
  1411→
  1412→    {
  1413→        std::ofstream f(cells_path, std::ios::binary);
  1414→        uint32_t current_offset = 0;
  1415→        for (const auto& [cell_id, ids] : sorted) {
  1416→            f.write(reinterpret_cast<const char*>(&cell_id), sizeof(cell_id));
  1417→            f.write(reinterpret_cast<const char*>(&current_offset), sizeof(current_offset));
  1418→            current_offset += sizeof(uint16_t) + ids.size() * sizeof(uint32_t);
  1419→        }
  1420→    }
  1421→
  1422→    {
  1423→        std::ofstream f(entries_path, std::ios::binary);
  1424→        for (const auto& [cell_id, ids] : sorted) {
  1425→            uint16_t count = static_cast<uint16_t>(std::min(ids.size(), size_t(65535)));
  1426→            f.write(reinterpret_cast<const char*>(&count), sizeof(count));
  1427→            f.write(reinterpret_cast<const char*>(ids.data()), ids.size() * sizeof(uint32_t));
  1428→        }
  1429→    }
  1430→}
  1431→
  1432→// --- Write all index files ---
  1433→
  1434→static void write_index(const ParsedData& data, const std::string& output_dir, IndexMode mode) {
  1435→    ensure_dir(output_dir);
  1436→    auto _wt = std::chrono::steady_clock::now();
  1437→
  1438→    bool write_streets = (mode != IndexMode::AdminOnly);
  1439→    bool write_addresses = (mode == IndexMode::Full);
  1440→
  1441→    if (write_streets) {
  1442→        // Build sorted_geo_cells from sorted pairs where available (avoids hash map iteration).
  1443→        // Extract unique cell IDs from each sorted pair vector in parallel, then merge.
  1444→        std::vector<uint64_t> sorted_geo_cells;
  1445→        {
  1446→            auto extract_unique_cells = [](const std::vector<CellItemPair>& pairs) {
  1447→                std::vector<uint64_t> cells;
  1448→                cells.reserve(pairs.size() / 2);
  1449→                for (size_t i = 0; i < pairs.size(); ) {
  1450→                    cells.push_back(pairs[i].cell_id);
  1451→                    uint64_t cur = pairs[i].cell_id;
  1452→                    while (i < pairs.size() && pairs[i].cell_id == cur) i++;
  1453→                }
  1454→                return cells;
  1455→            };
  1456→            auto extract_from_map = [](const std::unordered_map<uint64_t, std::vector<uint32_t>>& m) {
  1457→                std::vector<uint64_t> cells;
  1458→                cells.reserve(m.size());
  1459→                for (auto& [id, _] : m) cells.push_back(id);
  1460→                std::sort(cells.begin(), cells.end());
  1461→                return cells;
  1462→            };
  1463→
  1464→            // Extract unique cell IDs in parallel
  1465→            std::vector<uint64_t> way_cells, addr_cells, interp_cells;
  1466→            {
  1467→                auto f1 = std::async(std::launch::async, [&] {
  1468→                    return !data.sorted_way_cells.empty()
  1469→                        ? extract_unique_cells(data.sorted_way_cells)
  1470→                        : extract_from_map(data.cell_to_ways);
  1471→                });
  1472→                if (write_addresses) {
  1473→                    auto f2 = std::async(std::launch::async, [&] {
  1474→                        return !data.sorted_addr_cells.empty()
  1475→                            ? extract_unique_cells(data.sorted_addr_cells)
  1476→                            : extract_from_map(data.cell_to_addrs);
  1477→                    });
  1478→                    auto f3 = std::async(std::launch::async, [&] {
  1479→                        return extract_from_map(data.cell_to_interps);
  1480→                    });
  1481→                    addr_cells = f2.get();
  1482→                    interp_cells = f3.get();
  1483→                }
  1484→                way_cells = f1.get();
  1485→            }
  1486→
  1487→            // Merge sorted unique cell ID vectors
  1488→            sorted_geo_cells.reserve(way_cells.size() + addr_cells.size() + interp_cells.size());
  1489→            std::merge(way_cells.begin(), way_cells.end(),
  1490→                       addr_cells.begin(), addr_cells.end(),
  1491→                       std::back_inserter(sorted_geo_cells));
  1492→            if (!interp_cells.empty()) {
  1493→                std::vector<uint64_t> tmp;
  1494→                tmp.reserve(sorted_geo_cells.size() + interp_cells.size());
  1495→                std::merge(sorted_geo_cells.begin(), sorted_geo_cells.end(),
  1496→                           interp_cells.begin(), interp_cells.end(),
  1497→                           std::back_inserter(tmp));
  1498→                sorted_geo_cells = std::move(tmp);
  1499→            }
  1500→            // Deduplicate (merge may have duplicates from overlapping cells)
  1501→            sorted_geo_cells.erase(std::unique(sorted_geo_cells.begin(), sorted_geo_cells.end()),
  1502→                                    sorted_geo_cells.end());
  1503→        }
  1504→
  1505→        // Write entry files in parallel (returns positional offset vectors, not hash maps)
  1506→        std::vector<uint32_t> street_offsets, addr_offsets, interp_offsets;
  1507→        {
  1508→            auto f1 = std::async(std::launch::async, [&]() {
  1509→                // Use direct sorted-pair writing for ways (skips hash map extraction + sort)
  1510→                if (!data.sorted_way_cells.empty()) {
  1511→                    return write_entries_from_sorted(output_dir + "/street_entries.bin", sorted_geo_cells, data.sorted_way_cells);
  1512→                }
  1513→                return write_entries(output_dir + "/street_entries.bin", sorted_geo_cells, data.cell_to_ways);
  1514→            });
  1515→            if (write_addresses) {
  1516→                auto f2 = std::async(std::launch::async, [&]() {
  1517→                    if (!data.sorted_addr_cells.empty()) {
  1518→                        return write_entries_from_sorted(output_dir + "/addr_entries.bin", sorted_geo_cells, data.sorted_addr_cells);
  1519→                    }
  1520→                    return write_entries(output_dir + "/addr_entries.bin", sorted_geo_cells, data.cell_to_addrs);
  1521→                });
  1522→                auto f3 = std::async(std::launch::async, [&]() {
  1523→                    return write_entries(output_dir + "/interp_entries.bin", sorted_geo_cells, data.cell_to_interps);
  1524→                });
  1525→                addr_offsets = f2.get();
  1526→                interp_offsets = f3.get();
  1527→            }
  1528→            street_offsets = f1.get();
  1529→        }
  1530→
  1531→        log_phase("  Write: entry files", _wt);
  1532→        // Build geo_cells.bin — parallel buffer fill + single write
  1533→        {
  1534→            size_t n = sorted_geo_cells.size();
  1535→            size_t row_size = sizeof(uint64_t) + 3 * sizeof(uint32_t);
  1536→            std::vector<char> buf(n * row_size);
  1537→
  1538→            if (addr_offsets.empty()) addr_offsets.resize(n, NO_DATA);
  1539→            if (interp_offsets.empty()) interp_offsets.resize(n, NO_DATA);
  1540→
  1541→            // Parallel buffer fill — split into chunks across threads
  1542→            unsigned int nthreads = std::thread::hardware_concurrency();
  1543→            if (nthreads == 0) nthreads = 4;
  1544→            size_t chunk = (n + nthreads - 1) / nthreads;
  1545→            std::vector<std::thread> fill_threads;
  1546→            for (unsigned int t = 0; t < nthreads; t++) {
  1547→                size_t start = t * chunk;
  1548→                size_t end = std::min(start + chunk, n);
  1549→                if (start >= n) break;
  1550→                fill_threads.emplace_back([&, start, end]() {
  1551→                    char* ptr = buf.data() + start * row_size;
  1552→                    for (size_t i = start; i < end; i++) {
  1553→                        memcpy(ptr, &sorted_geo_cells[i], sizeof(uint64_t)); ptr += sizeof(uint64_t);
  1554→                        memcpy(ptr, &street_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
  1555→                        memcpy(ptr, &addr_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
  1556→                        memcpy(ptr, &interp_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
  1557→                    }
  1558→                });
  1559→            }
  1560→            for (auto& t : fill_threads) t.join();
  1561→
  1562→            std::ofstream f(output_dir + "/geo_cells.bin", std::ios::binary);
  1563→            f.write(buf.data(), buf.size());
  1564→        }
  1565→
  1566→        std::cerr << "geo index: " << sorted_geo_cells.size() << " cells ("
  1567→                  << data.ways.size() << " ways, "
  1568→                  << data.addr_points.size() << " addrs, "
  1569→                  << data.interp_ways.size() << " interps)" << std::endl;
  1570→    }
  1571→
  1572→    log_phase("  Write: geo_cells.bin", _wt);
  1573→    // Write admin cell index, raw data files, and admin polygons in parallel
  1574→    auto admin_future = std::async(std::launch::async, [&] {
  1575→        write_cell_index(output_dir + "/admin_cells.bin", output_dir + "/admin_entries.bin", data.cell_to_admin);
  1576→        std::cerr << "admin index: " << data.cell_to_admin.size() << " cells, " << data.admin_polygons.size() << " polygons" << std::endl;
  1577→    });
  1578→
  1579→    std::vector<std::future<void>> write_futures;
  1580→
  1581→    if (write_streets) {
  1582→        write_futures.push_back(std::async(std::launch::async, [&] {
  1583→            std::ofstream f(output_dir + "/street_ways.bin", std::ios::binary);
  1584→            f.write(reinterpret_cast<const char*>(data.ways.data()), data.ways.size() * sizeof(WayHeader));
  1585→        }));
  1586→        write_futures.push_back(std::async(std::launch::async, [&] {
  1587→            std::ofstream f(output_dir + "/street_nodes.bin", std::ios::binary);
  1588→            f.write(reinterpret_cast<const char*>(data.street_nodes.data()), data.street_nodes.size() * sizeof(NodeCoord));
  1589→        }));
  1590→        if (write_addresses) {
  1591→            write_futures.push_back(std::async(std::launch::async, [&] {
  1592→                std::ofstream f(output_dir + "/addr_points.bin", std::ios::binary);
  1593→                f.write(reinterpret_cast<const char*>(data.addr_points.data()), data.addr_points.size() * sizeof(AddrPoint));
  1594→            }));
  1595→            write_futures.push_back(std::async(std::launch::async, [&] {
  1596→                std::ofstream f(output_dir + "/interp_ways.bin", std::ios::binary);
  1597→                f.write(reinterpret_cast<const char*>(data.interp_ways.data()), data.interp_ways.size() * sizeof(InterpWay));
  1598→            }));
  1599→            write_futures.push_back(std::async(std::launch::async, [&] {
  1600→                std::ofstream f(output_dir + "/interp_nodes.bin", std::ios::binary);
  1601→                f.write(reinterpret_cast<const char*>(data.interp_nodes.data()), data.interp_nodes.size() * sizeof(NodeCoord));
  1602→            }));
  1603→        }
  1604→    }
  1605→
  1606→    write_futures.push_back(std::async(std::launch::async, [&] {
  1607→        std::ofstream f(output_dir + "/admin_polygons.bin", std::ios::binary);
  1608→        f.write(reinterpret_cast<const char*>(data.admin_polygons.data()), data.admin_polygons.size() * sizeof(AdminPolygon));
  1609→    }));
  1610→
  1611→    write_futures.push_back(std::async(std::launch::async, [&] {
  1612→        std::ofstream f(output_dir + "/admin_vertices.bin", std::ios::binary);
  1613→        f.write(reinterpret_cast<const char*>(data.admin_vertices.data()), data.admin_vertices.size() * sizeof(NodeCoord));
  1614→    }));
  1615→
  1616→    write_futures.push_back(std::async(std::launch::async, [&] {
  1617→        std::ofstream f(output_dir + "/strings.bin", std::ios::binary);
  1618→        f.write(data.string_pool.data().data(), data.string_pool.data().size());
  1619→        std::cerr << "strings.bin: " << data.string_pool.data().size() << " bytes" << std::endl;
  1620→    }));
  1621→
  1622→    // Wait for all parallel writes
  1623→    log_phase("  Write: parallel data files launched", _wt);
  1624→    admin_future.get();
  1625→    for (auto& f : write_futures) f.get();
  1626→}
  1627→
  1628→// --- Main ---
  1629→
  1630→int main(int argc, char* argv[]) {
  1631→    if (argc < 2) {
  1632→        std::cerr << "Usage: build-index <output-dir> <input.osm.pbf> [options]" << std::endl;
  1633→        std::cerr << "       build-index <output-dir> --load-cache <path> [options]" << std::endl;
  1634→        std::cerr << std::endl;
  1635→        std::cerr << "Options:" << std::endl;
  1636→        std::cerr << "  --street-level N       S2 cell level for streets (default: 17)" << std::endl;
  1637→        std::cerr << "  --admin-level N        S2 cell level for admin (default: 10)" << std::endl;
  1638→        std::cerr << "  --max-admin-level N    Only include admin levels <= N" << std::endl;
  1639→        std::cerr << "  --save-cache <path>    Save parsed data to cache file" << std::endl;
  1640→        std::cerr << "  --load-cache <path>    Load from cache instead of PBF" << std::endl;
  1641→        std::cerr << "  --multi-output         Write full, no-addresses, admin-only indexes" << std::endl;
  1642→        std::cerr << "  --continents           Also generate per-continent indexes" << std::endl;
  1643→        std::cerr << "  --mode <mode>          Index mode: full, no-addresses, admin-only (default: full)" << std::endl;


Now let me quickly scan the rest of main() to understand what it references.