     1→#pragma once
     2→
     3→#include <algorithm>
     4→#include <cmath>
     5→#include <cstdint>
     6→#include <cstring>
     7→#include <functional>
     8→#include <unordered_set>
     9→#include <vector>
    10→
    11→// Coordinate key matching osmium's Location (int32_t nanodegrees).
    12→// Two points at the same geographic location always produce the same key,
    13→// regardless of their OSM node IDs (handles split/replaced nodes).
    14→inline int64_t coord_key(double lat, double lng) {
    15→    int32_t ilat = static_cast<int32_t>(lat * 1e7 + (lat >= 0 ? 0.5 : -0.5));
    16→    int32_t ilng = static_cast<int32_t>(lng * 1e7 + (lng >= 0 ? 0.5 : -0.5));
    17→    return (static_cast<int64_t>(ilat) << 32) | static_cast<uint32_t>(ilng);
    18→}
    19→
    20→// Approximate polygon area in square degrees (shoelace formula)
    21→inline float polygon_area(const std::vector<std::pair<double,double>>& vertices) {
    22→    double area = 0;
    23→    size_t n = vertices.size();
    24→    for (size_t i = 0; i < n; i++) {
    25→        size_t j = (i + 1) % n;
    26→        area += vertices[i].first * vertices[j].second;
    27→        area -= vertices[j].first * vertices[i].second;
    28→    }
    29→    return static_cast<float>(std::fabs(area) / 2.0);
    30→}
    31→
    32→// Check if segment (a1,a2) crosses segment (b1,b2) using cross product orientation
    33→inline bool segments_intersect(double ax1, double ay1, double ax2, double ay2,
    34→                               double bx1, double by1, double bx2, double by2) {
    35→    auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) -> double {
    36→        return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);
    37→    };
    38→    double d1 = cross(bx1, by1, bx2, by2, ax1, ay1);
    39→    double d2 = cross(bx1, by1, bx2, by2, ax2, ay2);
    40→    double d3 = cross(ax1, ay1, ax2, ay2, bx1, by1);
    41→    double d4 = cross(ax1, ay1, ax2, ay2, bx2, by2);
    42→    if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
    43→        ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
    44→        return true;
    45→    }
    46→    return false;
    47→}
    48→
    49→// Sweep-line inspired self-intersection check for rings.
    50→// Brute force for small rings (<=32 segments), sweep-line pruning for large ones.
    51→inline bool ring_has_self_intersection(const std::vector<std::pair<double,double>>& ring) {
    52→    static constexpr size_t BRUTE_FORCE_THRESHOLD = 32;
    53→    size_t n = ring.size();
    54→    if (n < 4) return false;
    55→
    56→    if (n <= BRUTE_FORCE_THRESHOLD) {
    57→        for (size_t i = 0; i + 1 < n; i++) {
    58→            for (size_t j = i + 2; j + 1 < n; j++) {
    59→                if (j == i + 1 || (i == 0 && j == n - 2)) continue;
    60→                if (segments_intersect(ring[i].first, ring[i].second,
    61→                                       ring[i+1].first, ring[i+1].second,
    62→                                       ring[j].first, ring[j].second,
    63→                                       ring[j+1].first, ring[j+1].second))
    64→                    return true;
    65→            }
    66→        }
    67→        return false;
    68→    }
    69→
    70→    // For larger rings, sort segments by min-x and prune non-overlapping
    71→    struct Seg { double min_x, max_x; size_t idx; };
    72→    std::vector<Seg> segs(n - 1);
    73→    for (size_t i = 0; i + 1 < n; i++) {
    74→        double x1 = ring[i].second, x2 = ring[i+1].second; // use lng as x
    75→        segs[i] = {std::min(x1,x2), std::max(x1,x2), i};
    76→    }
    77→    std::sort(segs.begin(), segs.end(), [](const Seg& a, const Seg& b) {
    78→        return a.min_x < b.min_x;
    79→    });
    80→
    81→    for (size_t a = 0; a + 1 < segs.size(); a++) {
    82→        for (size_t b = a + 1; b < segs.size(); b++) {
    83→            if (segs[b].min_x > segs[a].max_x) break;
    84→            size_t i = segs[a].idx, j = segs[b].idx;
    85→            if (j == i + 1 || i == j + 1) continue;
    86→            if ((i == 0 && j == n - 2) || (j == 0 && i == n - 2)) continue;
    87→            if (segments_intersect(ring[i].first, ring[i].second,
    88→                                   ring[i+1].first, ring[i+1].second,
    89→                                   ring[j].first, ring[j].second,
    90→                                   ring[j+1].first, ring[j+1].second))
    91→                return true;
    92→        }
    93→    }
    94→    return false;
    95→}
    96→
    97→// Check if a ring has duplicate coordinates (spikes/figure-8 from merged holes).
    98→// A valid simple polygon visits each coordinate exactly once (except closing point).
    99→inline bool ring_has_duplicate_coords(const std::vector<std::pair<double,double>>& ring) {
   100→    std::unordered_set<int64_t> seen;
   101→    for (size_t i = 0; i + 1 < ring.size(); i++) {
   102→        int64_t k = coord_key(ring[i].first, ring[i].second);
   103→        if (!seen.insert(k).second) return true;
   104→    }
   105→    return false;
   106→}
   107→
   108→// Douglas-Peucker line simplification
   109→inline void dp_simplify(const std::vector<std::pair<double,double>>& pts,
   110→                        size_t start, size_t end, double epsilon,
   111→                        std::vector<bool>& keep) {
   112→    if (end <= start + 1) return;
   113→
   114→    double max_dist = 0;
   115→    size_t max_idx = start;
   116→
   117→    double ax = pts[start].first, ay = pts[start].second;
   118→    double bx = pts[end].first, by = pts[end].second;
   119→    double dx = bx - ax, dy = by - ay;
   120→    double len_sq = dx * dx + dy * dy;
   121→
   122→    for (size_t i = start + 1; i < end; i++) {
   123→        double px = pts[i].first - ax, py = pts[i].second - ay;
   124→        double dist;
   125→        if (len_sq == 0) {
   126→            dist = std::sqrt(px * px + py * py);
   127→        } else {
   128→            double t = std::max(0.0, std::min(1.0, (px * dx + py * dy) / len_sq));
   129→            double proj_x = t * dx - px, proj_y = t * dy - py;
   130→            dist = std::sqrt(proj_x * proj_x + proj_y * proj_y);
   131→        }
   132→        if (dist > max_dist) {
   133→            max_dist = dist;
   134→            max_idx = i;
   135→        }
   136→    }
   137→
   138→    if (max_dist > epsilon) {
   139→        keep[max_idx] = true;
   140→        dp_simplify(pts, start, max_idx, epsilon, keep);
   141→        dp_simplify(pts, max_idx, end, epsilon, keep);
   142→    }
   143→}
   144→
   145→static constexpr size_t MAX_POLYGON_VERTICES = 500;
   146→
   147→// Simplify to at most max_vertices (binary search for epsilon). Original behavior.
   148→inline std::vector<std::pair<double,double>> simplify_polygon(
   149→    const std::vector<std::pair<double,double>>& pts, size_t max_vertices = MAX_POLYGON_VERTICES) {
   150→    if (pts.size() <= max_vertices) return pts;
   151→
   152→    double lo = 0, hi = 1.0;
   153→    std::vector<std::pair<double,double>> result;
   154→
   155→    for (int iter = 0; iter < 20; iter++) {
   156→        double epsilon = (lo + hi) / 2;
   157→        std::vector<bool> keep(pts.size(), false);
   158→        keep[0] = true;
   159→        keep[pts.size() - 1] = true;
   160→        dp_simplify(pts, 0, pts.size() - 1, epsilon, keep);
   161→
   162→        size_t count = 0;
   163→        for (bool k : keep) if (k) count++;
   164→
   165→        if (count > max_vertices) {
   166→            lo = epsilon;
   167→        } else {
   168→            hi = epsilon;
   169→        }
   170→    }
   171→
   172→    std::vector<bool> keep(pts.size(), false);
   173→    keep[0] = true;
   174→    keep[pts.size() - 1] = true;
   175→    dp_simplify(pts, 0, pts.size() - 1, hi, keep);
   176→
   177→    result.clear();
   178→    for (size_t i = 0; i < pts.size(); i++) {
   179→        if (keep[i]) result.push_back(pts[i]);
   180→    }
   181→    return result;
   182→}
   183→
   184→// Error-bounded simplification: maximum displacement of epsilon_deg (in degrees).
   185→// Epsilon is in the same coordinate units as the vertices (degrees).
   186→// Converts meters to degrees using latitude: epsilon_deg = meters / (111320 * cos(lat))
   187→// Always keeps at least min_vertices (default 4) to preserve basic shape.
   188→inline std::vector<std::pair<double,double>> simplify_polygon_epsilon(
   189→    const std::vector<std::pair<double,double>>& pts, double epsilon_deg,
   190→    size_t min_vertices = 4) {
   191→    if (pts.size() <= min_vertices) return pts;
   192→
   193→    std::vector<bool> keep(pts.size(), false);
   194→    keep[0] = true;
   195→    keep[pts.size() - 1] = true;
   196→    dp_simplify(pts, 0, pts.size() - 1, epsilon_deg, keep);
   197→
   198→    size_t count = 0;
   199→    for (bool k : keep) if (k) count++;
   200→
   201→    // If simplification went below min_vertices, reduce epsilon until we have enough
   202→    if (count < min_vertices) {
   203→        double lo = 0, hi = epsilon_deg;
   204→        for (int iter = 0; iter < 20; iter++) {
   205→            double mid = (lo + hi) / 2;
   206→            std::vector<bool> try_keep(pts.size(), false);
   207→            try_keep[0] = true;
   208→            try_keep[pts.size() - 1] = true;
   209→            dp_simplify(pts, 0, pts.size() - 1, mid, try_keep);
   210→            size_t c = 0;
   211→            for (bool k : try_keep) if (k) c++;
   212→            if (c >= min_vertices) {
   213→                lo = mid;
   214→                keep = try_keep;
   215→                count = c;
   216→            } else {
   217→                hi = mid;
   218→            }
   219→        }
   220→    }
   221→
   222→    std::vector<std::pair<double,double>> result;
   223→    for (size_t i = 0; i < pts.size(); i++) {
   224→        if (keep[i]) result.push_back(pts[i]);
   225→    }
   226→    return result;
   227→}
   228→
   229→// Convert meters to degrees at a given latitude
   230→inline double meters_to_degrees(double meters, double latitude_deg) {
   231→    double cos_lat = std::cos(latitude_deg * M_PI / 180.0);
   232→    if (cos_lat < 0.01) cos_lat = 0.01; // clamp near poles
   233→    return meters / (111320.0 * cos_lat);
   234→}
   235→
   236→// Per-level epsilon defaults in meters. Index 0-1 unused, 2=country ... 8=city.
   237→// Can be overridden at runtime via kAdminEpsilonMeters and kEpsilonScale.
   238→inline double kDefaultEpsilon[] = {
   239→    15.0,   // 0 (unused)
   240→    15.0,   // 1 (unused)
   241→    500.0,  // 2 country
   242→    200.0,  // 3 large region
   243→    100.0,  // 4 state/province
   244→    50.0,   // 5 district
   245→    30.0,   // 6 county
   246→    20.0,   // 7 municipality
   247→    15.0,   // 8 city/town
   248→    15.0,   // 9
   249→    15.0,   // 10
   250→    15.0,   // 11 postal
   251→};
   252→
   253→// Runtime-configurable epsilon table and scale factor.
   254→// Set by main() from CLI args. Scale multiplies all values uniformly.
   255→inline double kAdminEpsilonMeters[12] = {
   256→    15, 15, 500, 200, 100, 50, 30, 20, 15, 15, 15, 15
   257→};
   258→inline double kEpsilonScale = 1.0;
   259→
   260→inline double admin_epsilon_meters(uint8_t admin_level) {
   261→    int idx = (admin_level <= 11) ? admin_level : 11;
   262→    return kAdminEpsilonMeters[idx] * kEpsilonScale;
   263→}
   264→
   265→// Parse leading digits from a house number string
   266→inline uint32_t parse_house_number(const char* s) {
   267→    if (!s) return 0;
   268→    uint32_t n = 0;
   269→    while (*s >= '0' && *s <= '9') {
   270→        n = n * 10 + (*s - '0');
   271→        s++;
   272→    }
   273→    return n;
   274→}
   275→
   276→// Highway types excluded from street indexing
   277→inline bool is_included_highway(const char* value) {
   278→    // Fast rejection by first character — avoids 8 strcmp calls for most values
   279→    switch (value[0]) {
   280→        case 'f': return std::strcmp(value, "footway") != 0;
   281→        case 'p': return std::strcmp(value, "path") != 0 && std::strcmp(value, "pedestrian") != 0;
   282→        case 't': return std::strcmp(value, "track") != 0;
   283→        case 's': return std::strcmp(value, "steps") != 0 && std::strcmp(value, "service") != 0;
   284→        case 'c': return std::strcmp(value, "cycleway") != 0 && std::strcmp(value, "construction") != 0;
   285→        case 'b': return std::strcmp(value, "bridleway") != 0;
   286→        default: return true;
   287→    }
   288→}
   289→


     1→#include <algorithm>
     2→#include <atomic>
     3→#include <chrono>
     4→#include <condition_variable>
     5→#include <cstring>
     6→#include <deque>
     7→#include <fstream>
     8→#include <future>
     9→#include <iostream>
    10→#include <mutex>
    11→#include <numeric>
    12→#include <string>
    13→#include <thread>
    14→#include <unordered_set>
    15→#include <vector>
    16→
    17→#include <fcntl.h>
    18→#include <sys/mman.h>
    19→
    20→#include "pbf_reader.h"
    21→
    22→#include <s2/s2cell_id.h>
    23→#include <s2/s2latlng.h>
    24→
    25→#include "types.h"
    26→#include "string_pool.h"
    27→#include "geometry.h"
    28→#include "parsed_data.h"
    29→#include "s2_helpers.h"
    30→#include "ring_assembly.h"
    31→#include "continent_boundaries.h"
    32→#include "interpolation.h"
    33→#include "cache.h"
    34→#include "continent_filter.h"
    35→#include "cell_index.h"
    36→
    37→
    38→// --- Main ---
    39→
    40→int main(int argc, char* argv[]) {
    41→    if (argc < 2) {
    42→        std::cerr << "Usage: build-index <output-dir> <input.osm.pbf> [options]" << std::endl;
    43→        std::cerr << "       build-index <output-dir> --load-cache <path> [options]" << std::endl;
    44→        std::cerr << std::endl;
    45→        std::cerr << "Options:" << std::endl;
    46→        std::cerr << "  --street-level N       S2 cell level for streets (default: 17)" << std::endl;
    47→        std::cerr << "  --admin-level N        S2 cell level for admin (default: 10)" << std::endl;
    48→        std::cerr << "  --max-admin-level N    Only include admin levels <= N" << std::endl;
    49→        std::cerr << "  --save-cache <path>    Save parsed data to cache file" << std::endl;
    50→        std::cerr << "  --load-cache <path>    Load from cache instead of PBF" << std::endl;
    51→        std::cerr << "  --multi-output         Write full, no-addresses, admin-only indexes" << std::endl;
    52→        std::cerr << "  --continents           Also generate per-continent indexes" << std::endl;
    53→        std::cerr << "  --mode <mode>          Index mode: full, no-addresses, admin-only (default: full)" << std::endl;
    54→        std::cerr << "  --admin-only           Shorthand for --mode admin-only" << std::endl;
    55→        std::cerr << "  --no-addresses         Shorthand for --mode no-addresses" << std::endl;
    56→        std::cerr << "  --simplify-epsilon     Use error-bounded simplification (per-level defaults)" << std::endl;
    57→        std::cerr << "  --simplify-epsilon N   Use fixed epsilon of N meters for all levels" << std::endl;
    58→        std::cerr << "  --epsilon-scale F      Multiply all per-level epsilons by F (e.g. 2.0 = 2x coarser)" << std::endl;
    59→        std::cerr << "  --epsilon-levels L2,L3,...,L8  Set epsilon in meters per level (7 values)" << std::endl;
    60→        std::cerr << "  --multi-quality [S,S,...]  Write quality variants at given scales (default: 0,0.2,0.5,1,1.5,2,2.5)" << std::endl;
    61→        std::cerr << "  --previous-build <dir>     Load previous build for stable string offsets (patchability)" << std::endl;
    62→        return 1;
    63→    }
    64→
    65→    std::string output_dir = argv[1];
    66→    std::vector<std::string> input_files;
    67→    std::string save_cache_path;
    68→    std::string load_cache_path;
    69→    std::string previous_build_path;
    70→    bool multi_output = false;
    71→    bool generate_continents = false;
    72→    IndexMode mode = IndexMode::Full;
    73→    SimplifyMode simplify_mode = SimplifyMode::MaxVertices;
    74→    double simplify_epsilon_override = 0; // 0 = use per-level defaults
    75→    bool multi_quality = false;
    76→    std::vector<double> quality_scales;
    77→
    78→    for (int i = 2; i < argc; i++) {
    79→        std::string arg = argv[i];
    80→        if (arg == "--street-level" && i + 1 < argc) {
    81→            kStreetCellLevel = std::atoi(argv[++i]);
    82→        } else if (arg == "--admin-level" && i + 1 < argc) {
    83→            kAdminCellLevel = std::atoi(argv[++i]);
    84→        } else if (arg == "--admin-only") {
    85→            mode = IndexMode::AdminOnly;
    86→        } else if (arg == "--no-addresses") {
    87→            mode = IndexMode::NoAddresses;
    88→        } else if (arg == "--max-admin-level" && i + 1 < argc) {
    89→            kMaxAdminLevel = std::atoi(argv[++i]);
    90→        } else if (arg == "--save-cache" && i + 1 < argc) {
    91→            save_cache_path = argv[++i];
    92→        } else if (arg == "--load-cache" && i + 1 < argc) {
    93→            load_cache_path = argv[++i];
    94→        } else if (arg == "--previous-build" && i + 1 < argc) {
    95→            previous_build_path = argv[++i];
    96→        } else if (arg == "--multi-output") {
    97→            multi_output = true;
    98→        } else if (arg == "--continents") {
    99→            generate_continents = true;
   100→        } else if (arg == "--simplify-epsilon") {


     1→#pragma once
     2→
     3→#include <condition_variable>
     4→#include <cstdint>
     5→#include <deque>
     6→#include <mutex>
     7→#include <thread>
     8→#include <unordered_map>
     9→#include <unordered_set>
    10→#include <vector>
    11→
    12→#include <s2/s2cell_id.h>
    13→
    14→#include "types.h"
    15→#include "geometry.h"
    16→#include "parsed_data.h"
    17→
    18→// --- S2 cell level globals (set by main arg parsing) ---
    19→
    20→extern int kStreetCellLevel;
    21→extern int kAdminCellLevel;
    22→extern int kMaxAdminLevel;
    23→
    24→// --- Simplification globals ---
    25→
    26→extern SimplifyMode kSimplifyMode;
    27→extern double kSimplifyEpsilonOverride; // 0 = use per-level defaults
    28→
    29→// --- S2 helpers ---
    30→
    31→void cover_edge(double lat1, double lng1, double lat2, double lng2,
    32→                std::vector<S2CellId>& out);
    33→
    34→std::vector<S2CellId> cover_edge(double lat1, double lng1, double lat2, double lng2);
    35→
    36→S2CellId point_to_cell(double lat, double lng);
    37→
    38→std::vector<std::pair<S2CellId, bool>> cover_polygon(
    39→    const std::vector<std::pair<double,double>>& vertices);
    40→
    41→// --- Thread pool for concurrent admin polygon S2 covering ---
    42→
    43→class AdminCoverPool {
    44→public:
    45→    struct WorkItem {
    46→        uint32_t poly_id;
    47→        std::vector<std::pair<double,double>> vertices;
    48→    };
    49→
    50→    explicit AdminCoverPool(size_t num_threads);
    51→    ~AdminCoverPool();
    52→
    53→    void submit(uint32_t poly_id, std::vector<std::pair<double,double>>&& vertices);
    54→    std::unordered_map<uint64_t, std::vector<uint32_t>> drain();
    55→
    56→private:
    57→    void worker_loop();
    58→
    59→    std::vector<std::thread> workers_;
    60→    std::deque<WorkItem> queue_;
    61→    std::vector<std::unordered_map<uint64_t, std::vector<uint32_t>>> thread_results_;
    62→    std::mutex mutex_;
    63→    std::condition_variable cv_;
    64→    std::condition_variable done_cv_;
    65→    size_t active_workers_ = 0;
    66→    bool stop_;
    67→};
    68→
    69→// --- Simplification dispatch ---
    70→
    71→// Simplify a polygon using the current global mode, taking admin_level into account.
    72→// In MaxVertices mode: caps at MAX_POLYGON_VERTICES (500).
    73→// In ErrorBounded mode: uses per-level epsilon in meters, converted to degrees.
    74→inline std::vector<std::pair<double,double>> simplify_admin_polygon(
    75→    const std::vector<std::pair<double,double>>& pts, uint8_t admin_level) {
    76→    if (kSimplifyMode == SimplifyMode::ErrorBounded) {
    77→        double eps_m = kSimplifyEpsilonOverride > 0
    78→            ? kSimplifyEpsilonOverride
    79→            : admin_epsilon_meters(admin_level);
    80→        // Estimate latitude from first vertex for degree conversion
    81→        double lat = pts.empty() ? 0.0 : pts[0].first;
    82→        double eps_deg = meters_to_degrees(eps_m, lat);
    83→        return simplify_polygon_epsilon(pts, eps_deg);
    84→    }
    85→    return simplify_polygon(pts);
    86→}
    87→
    88→// --- Helper functions that depend on S2 ---
    89→
    90→void add_addr_point(ParsedData& data, double lat, double lng,
    91→                    const char* housenumber, const char* street,
    92→                    uint64_t& addr_count_total);
    93→
    94→void add_admin_polygon(ParsedData& data,
    95→                       const std::vector<std::pair<double,double>>& vertices_in,
    96→                       const char* name, uint8_t admin_level,
    97→                       const char* country_code,
    98→                       AdminCoverPool* admin_pool = nullptr);
    99→


     1→#include <algorithm>
     2→#include <atomic>
     3→#include <cmath>
     4→#include <cstdint>
     5→#include <cstdio>
     6→#include <cstring>
     7→#include <fstream>
     8→#include <string>
     9→#include <thread>
    10→#include <vector>
    11→
    12→struct AdminPolygon {
    13→    uint32_t vertex_offset;
    14→    uint32_t vertex_count;
    15→    uint32_t name_id;
    16→    uint8_t admin_level;
    17→    char _pad1[3];
    18→    float area;
    19→    uint16_t country_code;
    20→    char _pad2[2];
    21→};
    22→static_assert(sizeof(AdminPolygon) == 24);
    23→
    24→struct Vert { float lat, lng; };
    25→
    26→static double dp_dist(Vert p, Vert a, Vert b) {
    27→    double dx = b.lat - a.lat, dy = b.lng - a.lng;
    28→    double len_sq = dx*dx + dy*dy;
    29→    if (len_sq == 0) {
    30→        double px = p.lat - a.lat, py = p.lng - a.lng;
    31→        return std::sqrt(px*px + py*py);
    32→    }
    33→    double t = std::max(0.0, std::min(1.0, ((p.lat-a.lat)*dx + (p.lng-a.lng)*dy) / len_sq));
    34→    double proj_x = a.lat + t*dx - p.lat;
    35→    double proj_y = a.lng + t*dy - p.lng;
    36→    return std::sqrt(proj_x*proj_x + proj_y*proj_y);
    37→}
    38→
    39→static void dp_mark(const std::vector<Vert>& pts, size_t s, size_t e, double eps, std::vector<bool>& keep) {
    40→    if (e <= s + 1) return;
    41→    double max_d = 0; size_t max_i = s;
    42→    for (size_t i = s+1; i < e; i++) {
    43→        double d = dp_dist(pts[i], pts[s], pts[e]);
    44→        if (d > max_d) { max_d = d; max_i = i; }
    45→    }
    46→    if (max_d > eps) {
    47→        keep[max_i] = true;
    48→        dp_mark(pts, s, max_i, eps, keep);
    49→        dp_mark(pts, max_i, e, eps, keep);
    50→    }


ToolSearch

Question: select:Grep


src/build_index.cpp-66-    std::vector<std::string> input_files;
src/build_index.cpp-67-    std::string save_cache_path;
src/build_index.cpp-68-    std::string load_cache_path;
src/build_index.cpp:69:    std::string previous_build_path;
/root/traccar-geocoder/builder/src/build_index.cpp-70-    bool multi_output = false;
/root/traccar-geocoder/builder/src/build_index.cpp-71-    bool generate_continents = false;
src/build_index.cpp-72-    IndexMode mode = IndexMode::Full;
--
/root/traccar-geocoder/builder/src/build_index.cpp-92-        } else if (arg == "--load-cache" && i + 1 < argc) {
/root/traccar-geocoder/builder/src/build_index.cpp-93-            load_cache_path = argv[++i];
/root/traccar-geocoder/builder/src/build_index.cpp-94-        } else if (arg == "--previous-build" && i + 1 < argc) {
src/build_index.cpp:95:            previous_build_path = argv[++i];
/root/traccar-geocoder/builder/src/build_index.cpp-96-        } else if (arg == "--multi-output") {
/root/traccar-geocoder/builder/src/build_index.cpp-97-            multi_output = true;
/root/traccar-geocoder/builder/src/build_index.cpp-98-        } else if (arg == "--continents") {
--
src/build_index.cpp-183-    auto _cpu = CpuTicks::now();
/root/traccar-geocoder/builder/src/build_index.cpp-184-
/root/traccar-geocoder/builder/src/build_index.cpp-185-    // Load previous build's string pool for stable offsets across builds
src/build_index.cpp:186:    if (!previous_build_path.empty()) {
src/build_index.cpp:187:        std::string prev_strings = previous_build_path + "/strings.bin";
/root/traccar-geocoder/builder/src/build_index.cpp-188-        if (data.string_pool.load_previous(prev_strings)) {
/root/traccar-geocoder/builder/src/build_index.cpp-189-            data.string_pool.mark_previous_loaded();
src/build_index.cpp-190-            std::cerr << "Loaded previous string pool: " << data.string_pool.previous_size()


/root/traccar-geocoder/builder/src/string_pool.h-25-    // Strings that exist in the old pool keep their original offset.
/root/traccar-geocoder/builder/src/string_pool.h-26-    // New strings get appended at the end.
/root/traccar-geocoder/builder/src/string_pool.h-27-    // This makes string offsets stable across builds, enabling efficient binary patches.
src/string_pool.h:28:    bool load_previous(const std::string& path) {
src/string_pool.h-29-        std::ifstream f(path, std::ios::binary | std::ios::ate);
/root/traccar-geocoder/builder/src/string_pool.h-30-        if (!f.is_open()) return false;
/root/traccar-geocoder/builder/src/string_pool.h-31-        size_t size = f.tellg();
--
/root/traccar-geocoder/builder/src/build_index.cpp-185-    // Load previous build's string pool for stable offsets across builds
/root/traccar-geocoder/builder/src/build_index.cpp-186-    if (!previous_build_path.empty()) {
src/build_index.cpp-187-        std::string prev_strings = previous_build_path + "/strings.bin";
src/build_index.cpp:188:        if (data.string_pool.load_previous(prev_strings)) {
/root/traccar-geocoder/builder/src/build_index.cpp-189-            data.string_pool.mark_previous_loaded();
src/build_index.cpp-190-            std::cerr << "Loaded previous string pool: " << data.string_pool.previous_size()
src/build_index.cpp-191-                      << " bytes from " << prev_strings << std::endl;


   180→
   181→    ParsedData data;
   182→    auto _pt = std::chrono::steady_clock::now();
   183→    auto _cpu = CpuTicks::now();
   184→
   185→    // Load previous build's string pool for stable offsets across builds
   186→    if (!previous_build_path.empty()) {
   187→        std::string prev_strings = previous_build_path + "/strings.bin";
   188→        if (data.string_pool.load_previous(prev_strings)) {
   189→            data.string_pool.mark_previous_loaded();
   190→            std::cerr << "Loaded previous string pool: " << data.string_pool.previous_size()
   191→                      << " bytes from " << prev_strings << std::endl;
   192→        } else {
   193→            std::cerr << "Warning: could not load previous string pool from " << prev_strings << std::endl;
   194→        }
   195→    }
   196→
   197→    if (!load_cache_path.empty()) {
   198→        // Load from cache
   199→        if (!deserialize_cache(data, load_cache_path)) {
   200→            std::cerr << "Error: failed to load cache" << std::endl;
   201→            return 1;
   202→        }
   203→    } else {
   204→        // Parse PBF files (always collect everything)
   205→        if (input_files.empty()) {
   206→            std::cerr << "Error: no input files specified and no --load-cache" << std::endl;
   207→            return 1;
   208→        }
   209→
   210→        // Create thread pool for concurrent admin polygon S2 covering
   211→        unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency() - 1);
   212→        std::cerr << "Using " << num_threads << " worker threads." << std::endl;
   213→        AdminCoverPool admin_pool(num_threads);
   214→        // BuildHandler no longer used — parallel processing handles everything
   215→
   216→        // Dense array node location index — lockless parallel writes
   217→        // Planet OSM node IDs max ~12.5 billion. 8 bytes per entry = 100GB virtual.
   218→        // MAP_NORESERVE means OS only allocates pages on write (~80GB for 10B nodes).
   219→        static const size_t MAX_NODE_ID = MAX_NODE_ID_DEFAULT;
   220→        struct PackedLocation {
   221→            int32_t lat_e7;  // latitude * 10^7
   222→            int32_t lon_e7;  // longitude * 10^7
   223→            bool valid() const { return lat_e7 != 0 || lon_e7 != 0; }
   224→            double lat() const { return lat_e7 / 10000000.0; }
   225→            double lon() const { return lon_e7 / 10000000.0; }
   226→        };
   227→        struct DenseIndex {
   228→            PackedLocation* data;
   229→            size_t capacity;


tools/simplify_compare.cpp:22:static_assert(sizeof(AdminPolygon) == 24);
tools/simplify_compare.cpp:117:    size_t poly_count = poly_buf.size() / sizeof(AdminPolygon);
test/test_ring_assembly.cpp:570:    ASSERT_TRUE(sizeof(WayHeader) > 0);
test/test_ring_assembly.cpp:571:    ASSERT_TRUE(sizeof(WayHeader) <= 16); // should be compact
src/cell_index.cpp:296:            f.write(reinterpret_cast<const char*>(data.ways.data()), data.ways.size() * sizeof(WayHeader));
src/cell_index.cpp:305:                f.write(reinterpret_cast<const char*>(data.addr_points.data()), data.addr_points.size() * sizeof(AddrPoint));
src/cell_index.cpp:309:                f.write(reinterpret_cast<const char*>(data.interp_ways.data()), data.interp_ways.size() * sizeof(InterpWay));
src/cell_index.cpp:363:        f.write(reinterpret_cast<const char*>(admin_to_write.data()), admin_to_write.size() * sizeof(AdminPolygon));
src/cell_index.cpp:459:        f.write(reinterpret_cast<const char*>(new_polys.data()), new_polys.size() * sizeof(AdminPolygon));


Bash

cat > /dev/null 2>&1; grep -n "static_assert\|sizeof\|__attribute__\|#pragma pack\|padding\|_pad" src/types.h test/compare_indexes.cpp tools/simplify_compare.cpp 2>/dev/null || true

Search for struct size assertions and padding in key files


test/compare_indexes.cpp:79:    size_t count = data.size() / sizeof(T);
test/compare_indexes.cpp:81:    memcpy(result.data(), data.data(), count * sizeof(T));
tools/simplify_compare.cpp:17:    char _pad1[3];
tools/simplify_compare.cpp:20:    char _pad2[2];
tools/simplify_compare.cpp:22:static_assert(sizeof(AdminPolygon) == 24);
tools/simplify_compare.cpp:117:    size_t poly_count = poly_buf.size() / sizeof(AdminPolygon);
tools/simplify_compare.cpp:126:    fprintf(stderr, "Loaded %zu polygons, %zu vertices\n", poly_count, vert_buf.size()/sizeof(Vert));
tools/simplify_compare.cpp:166:            char label[64]; snprintf(label, sizeof(label), "max %zu", max_v);
tools/simplify_compare.cpp:190:            char label[64]; snprintf(label, sizeof(label), "%.0fm fixed", eps_m);
tools/simplify_compare.cpp:272:                char mode[64]; snprintf(mode, sizeof(mode), "per-level (%.0fm)", eps_m);
tools/simplify_compare.cpp:286:                char mode[64]; snprintf(mode, sizeof(mode), "%.0fm fixed", fixed[li]);


tools/simplify_compare.cpp:17:    char _pad1[3];
tools/simplify_compare.cpp:20:    char _pad2[2];
tools/simplify_compare.cpp:22:static_assert(sizeof(AdminPolygon) == 24);


32:    uint32_t vertex_count;
62:static constexpr int    MAX_VERTEX_COUNT        = 0x7FFFFFFF; // uint32_t vertex_count


47:    uint16_t vertex_count;
226:        for (uint16_t i = 0; i < p.vertex_count; i++) {


/root/traccar-geocoder/server/src/auth.rs
/root/traccar-geocoder/server/src/main.rs


     1→#include "cache.h"
     2→
     3→#include <cstring>
     4→#include <fstream>
     5→#include <iostream>
     6→
     7→enum class SectionType : uint32_t {
     8→    STRING_POOL = 0, WAYS = 1, STREET_NODES = 2, CELL_TO_WAYS = 3,
     9→    ADDR_POINTS = 4, CELL_TO_ADDRS = 5, INTERP_WAYS = 6, INTERP_NODES = 7,
    10→    CELL_TO_INTERPS = 8, ADMIN_POLYGONS = 9, ADMIN_VERTICES = 10, CELL_TO_ADMIN = 11,
    11→};
    12→
    13→static const char CACHE_MAGIC[8] = {'T','G','C','A','C','H','E','\0'};
    14→static const uint32_t CACHE_VERSION = 1;
    15→static const uint32_t CACHE_SECTION_COUNT = 12;
    16→
    17→template<typename T>
    18→static std::vector<char> serialize_vector(const std::vector<T>& vec) {
    19→    std::vector<char> buf;
    20→    uint64_t count = vec.size();
    21→    buf.resize(sizeof(uint64_t) + count * sizeof(T));
    22→    std::memcpy(buf.data(), &count, sizeof(uint64_t));
    23→    if (count > 0) std::memcpy(buf.data() + sizeof(uint64_t), vec.data(), count * sizeof(T));
    24→    return buf;
    25→}
    26→
    27→template<typename T>
    28→static bool deserialize_vector(const char* data, uint64_t length, std::vector<T>& vec) {
    29→    if (length < sizeof(uint64_t)) return false;
    30→    uint64_t count;
    31→    std::memcpy(&count, data, sizeof(uint64_t));
    32→    if (length < sizeof(uint64_t) + count * sizeof(T)) return false;
    33→    vec.resize(count);
    34→    if (count > 0) std::memcpy(vec.data(), data + sizeof(uint64_t), count * sizeof(T));
    35→    return true;
    36→}
    37→
    38→static std::vector<char> serialize_cell_map(const std::unordered_map<uint64_t, std::vector<uint32_t>>& map) {
    39→    size_t total = sizeof(uint64_t);
    40→    for (const auto& [cell_id, ids] : map)
    41→        total += sizeof(uint64_t) + sizeof(uint32_t) + ids.size() * sizeof(uint32_t);
    42→    std::vector<char> buf(total);
    43→    char* ptr = buf.data();
    44→    uint64_t entry_count = map.size();
    45→    std::memcpy(ptr, &entry_count, sizeof(uint64_t)); ptr += sizeof(uint64_t);
    46→    for (const auto& [cell_id, ids] : map) {
    47→        std::memcpy(ptr, &cell_id, sizeof(uint64_t)); ptr += sizeof(uint64_t);
    48→        uint32_t id_count = static_cast<uint32_t>(ids.size());
    49→        std::memcpy(ptr, &id_count, sizeof(uint32_t)); ptr += sizeof(uint32_t);
    50→        if (id_count > 0) { std::memcpy(ptr, ids.data(), id_count * sizeof(uint32_t)); ptr += id_count * sizeof(uint32_t); }
    51→    }
    52→    return buf;
    53→}
    54→
    55→static bool deserialize_cell_map(const char* data, uint64_t length,
    56→                                  std::unordered_map<uint64_t, std::vector<uint32_t>>& map) {
    57→    if (length < sizeof(uint64_t)) return false;
    58→    const char* ptr = data;
    59→    const char* end = data + length;
    60→    uint64_t entry_count;


     1→mod auth;
     2→
     3→use axum::extract::Query;
     4→use axum::http::StatusCode;
     5→use axum::response::{IntoResponse, Response};
     6→use axum::routing::get;
     7→use axum::Router;
     8→use memmap2::Mmap;
     9→use s2::cellid::CellID;
    10→use s2::latlng::LatLng;
    11→use serde::{Deserialize, Serialize};
    12→use std::borrow::Cow;
    13→use std::fs::File;
    14→use std::sync::{Arc, RwLock};
    15→
    16→// --- S2 helpers ---
    17→
    18→const DEFAULT_STREET_CELL_LEVEL: u64 = 17;
    19→const DEFAULT_ADMIN_CELL_LEVEL: u64 = 10;
    20→const DEFAULT_SEARCH_DISTANCE: f64 = 75.0;
    21→
    22→fn cell_id_at_level(lat: f64, lng: f64, level: u64) -> u64 {
    23→    let ll = LatLng::from_degrees(lat, lng);
    24→    CellID::from(ll).parent(level).0
    25→}
    26→
    27→fn cell_neighbors_at_level(cell_id: u64, level: u64) -> Vec<u64> {
    28→    let cell = CellID(cell_id);
    29→    cell.all_neighbors(level).into_iter().map(|c| c.0).collect()
    30→}
    31→
    32→// --- Binary format structs (must match C++ build pipeline) ---
    33→
    34→#[repr(C)]
    35→#[derive(Clone, Copy)]
    36→struct WayHeader {
    37→    node_offset: u32,
    38→    node_count: u8,
    39→    name_id: u32,
    40→}
    41→
    42→#[repr(C)]
    43→#[derive(Clone, Copy)]
    44→struct AddrPoint {
    45→    lat: f32,
    46→    lng: f32,
    47→    housenumber_id: u32,
    48→    street_id: u32,
    49→}
    50→
    51→#[repr(C)]
    52→#[derive(Clone, Copy)]
    53→struct InterpWay {
    54→    node_offset: u32,
    55→    node_count: u8,
    56→    street_id: u32,
    57→    start_number: u32,
    58→    end_number: u32,
    59→    interpolation: u8,
    60→}
    61→
    62→#[repr(C)]
    63→#[derive(Clone, Copy)]
    64→struct AdminPolygon {
    65→    vertex_offset: u32,
    66→    vertex_count: u32,
    67→    name_id: u32,
    68→    admin_level: u8,
    69→    area: f32,
    70→    country_code: u16,
    71→}
    72→
    73→#[repr(C)]
    74→#[derive(Clone, Copy)]
    75→struct NodeCoord {
    76→    lat: f32,
    77→    lng: f32,
    78→}
    79→
    80→// --- Index data ---
    81→
    82→struct Index {
    83→    geo_cells: Option<Mmap>,
    84→    street_entries: Option<Mmap>,
    85→    street_ways: Option<Mmap>,
    86→    street_nodes: Option<Mmap>,
    87→    addr_entries: Option<Mmap>,
    88→    addr_points: Option<Mmap>,
    89→    interp_entries: Option<Mmap>,
    90→    interp_ways: Option<Mmap>,
    91→    interp_nodes: Option<Mmap>,
    92→    admin_cells: Mmap,
    93→    admin_entries: Mmap,
    94→    admin_polygons: Mmap,
    95→    admin_vertices: Mmap,
    96→    strings: Mmap,
    97→    street_cell_level: u64,
    98→    admin_cell_level: u64,
    99→    max_distance_sq: f64,
   100→}


   100→}
   101→
   102→const NO_DATA: u32 = 0xFFFFFFFF;
   103→
   104→struct GeoCellOffsets {
   105→    street: u32,
   106→    addr: u32,
   107→    interp: u32,
   108→}
   109→
   110→fn mmap_file(path: &str) -> Result<Mmap, String> {
   111→    let file = File::open(path).map_err(|e| format!("Failed to open {}: {}", path, e))?;
   112→    unsafe { Mmap::map(&file).map_err(|e| format!("Failed to mmap {}: {}", path, e)) }
   113→}
   114→
   115→fn mmap_file_optional(path: &str) -> Option<Mmap> {
   116→    let file = File::open(path).ok()?;
   117→    unsafe { Mmap::map(&file).ok() }
   118→}
   119→
   120→impl Index {
   121→    fn load(dir: &str, street_cell_level: u64, admin_cell_level: u64, search_distance: f64) -> Result<Self, String> {
   122→        let meters_to_rad = search_distance / 111_320.0;
   123→        let max_distance_sq = meters_to_rad * meters_to_rad;
   124→
   125→        let geo_cells = mmap_file_optional(&format!("{}/geo_cells.bin", dir));
   126→        let has_geo = geo_cells.is_some();
   127→
   128→        let required_geo = |name: &str| -> Result<Option<Mmap>, String> {
   129→            let path = format!("{}/{}", dir, name);
   130→            let mmap = mmap_file_optional(&path);
   131→            if has_geo && mmap.is_none() {
   132→                return Err(format!("Partial geo index: {} is missing", name));
   133→            }
   134→            Ok(mmap)
   135→        };
   136→
   137→        Ok(Index {
   138→            geo_cells,
   139→            street_entries: required_geo("street_entries.bin")?,
   140→            street_ways: required_geo("street_ways.bin")?,
   141→            street_nodes: required_geo("street_nodes.bin")?,
   142→            addr_entries: mmap_file_optional(&format!("{}/addr_entries.bin", dir)),
   143→            addr_points: mmap_file_optional(&format!("{}/addr_points.bin", dir)),
   144→            interp_entries: mmap_file_optional(&format!("{}/interp_entries.bin", dir)),
   145→            interp_ways: mmap_file_optional(&format!("{}/interp_ways.bin", dir)),
   146→            interp_nodes: mmap_file_optional(&format!("{}/interp_nodes.bin", dir)),
   147→            admin_cells: mmap_file(&format!("{}/admin_cells.bin", dir))?,
   148→            admin_entries: mmap_file(&format!("{}/admin_entries.bin", dir))?,
   149→            admin_polygons: mmap_file(&format!("{}/admin_polygons.bin", dir))?,
   150→            admin_vertices: mmap_file(&format!("{}/admin_vertices.bin", dir))?,
   151→            strings: mmap_file(&format!("{}/strings.bin", dir))?,
   152→            street_cell_level,
   153→            admin_cell_level,
   154→            max_distance_sq,
   155→        })
   156→    }
   157→
   158→    fn get_string(&self, offset: u32) -> &str {
   159→        let bytes = &self.strings[offset as usize..];
   160→        let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
   161→        std::str::from_utf8(&bytes[..end]).unwrap_or("")
   162→    }
   163→
   164→    fn read_u16(data: &[u8], offset: usize) -> u16 {
   165→        u16::from_le_bytes([data[offset], data[offset + 1]])
   166→    }
   167→
   168→    fn read_u32(data: &[u8], offset: usize) -> u32 {
   169→        u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap())
   170→    }
   171→
   172→    fn read_u64(data: &[u8], offset: usize) -> u64 {
   173→        u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap())
   174→    }
   175→
   176→    // Iterate entry IDs inline from entries file at given offset
   177→    fn for_each_entry(entries: &[u8], offset: u32, mut f: impl FnMut(u32)) {
   178→        if offset == NO_DATA { return; }
   179→        let offset = offset as usize;
   180→        if offset + 2 > entries.len() { return; }
   181→
   182→        let id_count = Self::read_u16(entries, offset) as usize;
   183→        let data_start = offset + 2;
   184→        if data_start + id_count * 4 > entries.len() { return; }
   185→
   186→        for i in 0..id_count {
   187→            f(Self::read_u32(entries, data_start + i * 4));
   188→        }
   189→    }
   190→
   191→    // Binary search geo cell index: 20 bytes per entry (u64 cell_id + u32 street + u32 addr + u32 interp)
   192→    fn lookup_geo_cell(cells: &[u8], cell_id: u64) -> GeoCellOffsets {
   193→        let entry_size: usize = 20;
   194→        let count = cells.len() / entry_size;
   195→        let empty = GeoCellOffsets { street: NO_DATA, addr: NO_DATA, interp: NO_DATA };
   196→        if count == 0 { return empty; }
   197→
   198→        let mut lo = 0usize;
   199→        let mut hi = count;
   200→        while lo < hi {
   201→            let mid = lo + (hi - lo) / 2;
   202→            let mid_id = Self::read_u64(cells, mid * entry_size);
   203→            if mid_id == cell_id {
   204→                return GeoCellOffsets {
   205→                    street: Self::read_u32(cells, mid * entry_size + 8),
   206→                    addr: Self::read_u32(cells, mid * entry_size + 12),
   207→                    interp: Self::read_u32(cells, mid * entry_size + 16),
   208→                };
   209→            } else if mid_id < cell_id {
   210→                lo = mid + 1;
   211→            } else {
   212→                hi = mid;
   213→            }
   214→        }
   215→        empty
   216→    }
   217→
   218→    // Binary search admin cell index: 12 bytes per entry (u64 cell_id + u32 offset)
   219→    fn lookup_admin_cell(cells: &[u8], cell_id: u64) -> u32 {
   220→        let entry_size: usize = 12;
   221→        let count = cells.len() / entry_size;
   222→        if count == 0 { return NO_DATA; }
   223→
   224→        let mut lo = 0usize;
   225→        let mut hi = count;
   226→        while lo < hi {
   227→            let mid = lo + (hi - lo) / 2;
   228→            let mid_id = Self::read_u64(cells, mid * entry_size);
   229→            if mid_id == cell_id {
   230→                return Self::read_u32(cells, mid * entry_size + 8);
   231→            } else if mid_id < cell_id {
   232→                lo = mid + 1;
   233→            } else {
   234→                hi = mid;
   235→            }
   236→        }
   237→        NO_DATA
   238→    }
   239→
   240→    // --- Geo lookup (streets, addresses, interpolation from merged index) ---
   241→
   242→    fn query_geo(&self, lat: f64, lng: f64) -> (Option<(f64, &AddrPoint)>, Option<(f64, &str, u32)>, Option<(f64, &WayHeader)>) {
   243→        let geo_cells = match &self.geo_cells {
   244→            Some(gc) => gc,
   245→            None => return (None, None, None),
   246→        };
   247→        let street_entries = self.street_entries.as_ref().unwrap();
   248→        let street_ways_mmap = self.street_ways.as_ref().unwrap();
   249→        let street_nodes_mmap = self.street_nodes.as_ref().unwrap();


   250→        let cell = cell_id_at_level(lat, lng, self.street_cell_level);
   251→        let neighbors = cell_neighbors_at_level(cell, self.street_cell_level);
   252→
   253→        let empty_addr: &[AddrPoint] = &[];
   254→        let all_points: &[AddrPoint] = if let Some(ref m) = self.addr_points {
   255→            unsafe {
   256→                std::slice::from_raw_parts(
   257→                    m.as_ptr() as *const AddrPoint,
   258→                    m.len() / std::mem::size_of::<AddrPoint>(),
   259→                )
   260→            }
   261→        } else {
   262→            empty_addr
   263→        };
   264→        let all_ways: &[WayHeader] = unsafe {
   265→            std::slice::from_raw_parts(
   266→                street_ways_mmap.as_ptr() as *const WayHeader,
   267→                street_ways_mmap.len() / std::mem::size_of::<WayHeader>(),
   268→            )
   269→        };
   270→        let all_street_nodes: &[NodeCoord] = unsafe {
   271→            std::slice::from_raw_parts(
   272→                street_nodes_mmap.as_ptr() as *const NodeCoord,
   273→                street_nodes_mmap.len() / std::mem::size_of::<NodeCoord>(),
   274→            )
   275→        };
   276→        let empty_interp: &[InterpWay] = &[];
   277→        let all_interps: &[InterpWay] = if let Some(ref m) = self.interp_ways {
   278→            unsafe {
   279→                std::slice::from_raw_parts(
   280→                    m.as_ptr() as *const InterpWay,
   281→                    m.len() / std::mem::size_of::<InterpWay>(),
   282→                )
   283→            }
   284→        } else {
   285→            empty_interp
   286→        };
   287→        let empty_interp_nodes: &[NodeCoord] = &[];
   288→        let all_interp_nodes: &[NodeCoord] = if let Some(ref m) = self.interp_nodes {
   289→            unsafe {
   290→                std::slice::from_raw_parts(
   291→                    m.as_ptr() as *const NodeCoord,
   292→                    m.len() / std::mem::size_of::<NodeCoord>(),
   293→                )
   294→            }
   295→        } else {
   296→            empty_interp_nodes
   297→        };
   298→
   299→        let cos_lat = lat.to_radians().cos();
   300→
   301→        let mut best_addr_dist = f64::MAX;
   302→        let mut best_addr: Option<&AddrPoint> = None;
   303→        let mut best_street_dist = f64::MAX;
   304→        let mut best_street: Option<&WayHeader> = None;
   305→        let mut best_interp_dist = f64::MAX;
   306→        let mut best_interp: Option<&InterpWay> = None;
   307→        let mut best_interp_t: f64 = 0.0;
   308→
   309→        // Fixed-size hash set on stack to skip duplicate street IDs across cells
   310→        let mut seen_streets: [u32; 64] = [u32::MAX; 64];
   311→
   312→        for c in std::iter::once(cell).chain(neighbors.into_iter()) {
   313→            let offsets = Self::lookup_geo_cell(geo_cells, c);
   314→
   315→            // Addresses
   316→            if let Some(ref addr_entries) = self.addr_entries {
   317→                Self::for_each_entry(addr_entries, offsets.addr, |id| {
   318→                    let point = &all_points[id as usize];
   319→                    let dlat = (point.lat as f64 - lat).to_radians();
   320→                    let dlng = (point.lng as f64 - lng).to_radians();
   321→                    let dist = dist_sq(dlat, dlng, cos_lat);
   322→                    if dist < best_addr_dist {
   323→                        best_addr_dist = dist;
   324→                        best_addr = Some(point);
   325→                    }
   326→                });
   327→            }
   328→
   329→            // Streets
   330→            Self::for_each_entry(street_entries, offsets.street, |id| {
   331→                let slot = (id as usize) & 0x3F;
   332→                if seen_streets[slot] == id { return; }
   333→                seen_streets[slot] = id;
   334→
   335→                let way = &all_ways[id as usize];
   336→                let offset = way.node_offset as usize;
   337→                let count = way.node_count as usize;
   338→                let nodes = &all_street_nodes[offset..offset + count];
   339→
   340→                for i in 0..nodes.len() - 1 {
   341→                    let dist = point_to_segment_distance(
   342→                        lat, lng,
   343→                        nodes[i].lat as f64, nodes[i].lng as f64,
   344→                        nodes[i + 1].lat as f64, nodes[i + 1].lng as f64,
   345→                        cos_lat,
   346→                    );
   347→                    if dist < best_street_dist {
   348→                        best_street_dist = dist;
   349→                        best_street = Some(way);
   350→                    }
   351→                }
   352→            });
   353→
   354→            // Interpolation
   355→            if let Some(ref interp_entries) = self.interp_entries {
   356→                Self::for_each_entry(interp_entries, offsets.interp, |id| {
   357→                    let iw = &all_interps[id as usize];
   358→                    if iw.start_number == 0 || iw.end_number == 0 { return; }
   359→
   360→                    let offset = iw.node_offset as usize;
   361→                    let count = iw.node_count as usize;
   362→                    let nodes = &all_interp_nodes[offset..offset + count];
   363→
   364→                    let mut total_len: f64 = 0.0;
   365→                    for i in 0..nodes.len() - 1 {
   366→                        let dlat = (nodes[i + 1].lat as f64 - nodes[i].lat as f64).to_radians();
   367→                        let dlng = (nodes[i + 1].lng as f64 - nodes[i].lng as f64).to_radians();
   368→                        total_len += dist_sq(dlat, dlng, cos_lat);
   369→                    }
   370→                    if total_len == 0.0 { return; }
   371→
   372→                    let mut best_seg_dist = f64::MAX;
   373→                    let mut best_seg_t: f64 = 0.0;
   374→                    let mut prev_accumulated: f64 = 0.0;
   375→
   376→                    for i in 0..nodes.len() - 1 {
   377→                        let dlat = (nodes[i + 1].lat as f64 - nodes[i].lat as f64).to_radians();
   378→                        let dlng = (nodes[i + 1].lng as f64 - nodes[i].lng as f64).to_radians();
   379→                        let seg_len = dist_sq(dlat, dlng, cos_lat);
   380→                        let (dist, seg_t) = point_to_segment_with_t(
   381→                            lat, lng,
   382→                            nodes[i].lat as f64, nodes[i].lng as f64,
   383→                            nodes[i + 1].lat as f64, nodes[i + 1].lng as f64,
   384→                            cos_lat,
   385→                        );
   386→                        if dist < best_seg_dist {
   387→                            best_seg_dist = dist;
   388→                            best_seg_t = (prev_accumulated + seg_t * seg_len) / total_len;
   389→                        }
   390→                        prev_accumulated += seg_len;
   391→                    }
   392→
   393→                    if best_seg_dist < best_interp_dist {
   394→                        best_interp_dist = best_seg_dist;
   395→                        best_interp = Some(iw);
   396→                        best_interp_t = best_seg_t;
   397→                    }
   398→                });
   399→            }


     1→#pragma once
     2→
     3→#include <string>
     4→
     5→#include "parsed_data.h"
     6→
     7→void serialize_cache(const ParsedData& data, const std::string& path);
     8→bool deserialize_cache(ParsedData& data, const std::string& path);
     9→


62-#[repr(C)]
63-#[derive(Clone, Copy)]
64:struct AdminPolygon {
65:    vertex_offset: u32,
66-    vertex_count: u32,
67-    name_id: u32,
68:    admin_level: u8,
69-    area: f32,
70-    country_code: u16,
--
92-    admin_cells: Mmap,
93-    admin_entries: Mmap,
94:    admin_polygons: Mmap,
95-    admin_vertices: Mmap,
96-    strings: Mmap,
--
147-            admin_cells: mmap_file(&format!("{}/admin_cells.bin", dir))?,
148-            admin_entries: mmap_file(&format!("{}/admin_entries.bin", dir))?,
149:            admin_polygons: mmap_file(&format!("{}/admin_polygons.bin", dir))?,
150-            admin_vertices: mmap_file(&format!("{}/admin_vertices.bin", dir))?,
151-            strings: mmap_file(&format!("{}/strings.bin", dir))?,
--
432-        let neighbors = cell_neighbors_at_level(cell, self.admin_cell_level);
433-
434:        let all_polygons: &[AdminPolygon] = unsafe {
435-            std::slice::from_raw_parts(
436:                self.admin_polygons.as_ptr() as *const AdminPolygon,
437:                self.admin_polygons.len() / std::mem::size_of::<AdminPolygon>(),
438-            )
439-        };
--
446-
447-        // For each admin level, find the smallest-area polygon containing the point
448:        let mut best_by_level: [Option<(f32, &AdminPolygon)>; 12] = [None; 12];
449-
450-        const INTERIOR_FLAG: u32 = 0x80000000;
--
456-                let poly_id = (id & ID_MASK) as usize;
457-                let poly = &all_polygons[poly_id];
458:                let level = poly.admin_level as usize;
459-                if level >= 12 { return; }
460-
--
466-                // Interior cells skip point-in-polygon test
467-                if is_interior || point_in_polygon(lat as f32, lng as f32, {
468:                    let offset = poly.vertex_offset as usize;
469-                    let count = poly.vertex_count as usize;
470-                    &all_vertices[offset..offset + count]
--
480-            if let Some((_, poly)) = best_by_level[level] {
481-                let name = self.get_string(poly.name_id);
482:                match poly.admin_level {
483-                    2 => {
484-                        result.country = Some(name);


     1→#pragma once
     2→
     3→#include "parsed_data.h"
     4→
     5→void resolve_interpolation_endpoints(ParsedData& data);
     6→


List data directory contents


total 49
drwxr-xr-x 2 root root   10 Mar 26 01:23 .
drwxr-xr-x 8 root root   10 Mar 26 20:19 ..
-rw-r--r-- 1 root root 1304 Mar 26 01:23 africa.poly
-rw-r--r-- 1 root root  178 Mar 26 01:23 antarctica.poly
-rw-r--r-- 1 root root 1167 Mar 26 01:23 asia.poly
-rw-r--r-- 1 root root  510 Mar 26 01:23 australia-oceania.poly
-rw-r--r-- 1 root root 3058 Mar 26 01:23 central-america.poly
-rw-r--r-- 1 root root 2924 Mar 26 01:23 europe.poly
-rw-r--r-- 1 root root 2476 Mar 26 01:23 north-america.poly
-rw-r--r-- 1 root root  888 Mar 26 01:23 south-america.poly


     1→# Build Optimization Log
     2→
     3→## Final Result: 54m → 12m 22s (77% faster)
     4→
     5→Build: `--multi-output --continents --multi-quality`
     6→Output: 379 files, 91 GiB (planet + 8 continents × 3 modes × 7 quality levels)
     7→Machine: Node 3 — 32-core / 174 GiB RAM
     8→Validated: planet output identical to pre-optimization baseline
     9→
    10→## Phase Breakdown (Node 3)
    11→
    12→| Phase | Baseline (54m) | Final (12m 22s) | Speedup |
    13→|---|---|---|---|
    14→| Pass 1: relations | 46.6s | **3.9-7s** | 7-12x |
    15→| Pass 2: nodes | 184.4s | **80-106s** | 1.7-2.3x |
    16→| Pass 2b: ways | 132.3s | **88-115s** | 1.1-1.5x |
    17→| Admin S2 drain | 134.4s | 109-132s | ~same |
    18→| S2 street ways | 136.9s | 129-155s | ~same |
    19→| Rebuild cell maps | 224.2s | **0s** | eliminated |
    20→| Dedup | (in S2) | 38-55s | — |
    21→| Continent processing | 2087.5s | **60-131s** | 16-35x |
    22→| **Total** | **54m 1.7s** | **12m 22s** | **4.4x** |
    23→
    24→## All Optimizations (30+)
    25→
    26→### Custom PBF Reader (replaced osmium)
    27→1. Parallel PBF reader with per-thread fds and pread
    28→2. Streaming node decode — no PbfNode objects, inline callback
    29→3. Streaming way decode — no PbfWay objects, inline callback
    30→4. Zero-copy varint decode for dense nodes
    31→5. Binary search blob classification (32 decompressions vs 50K)
    32→6. Reusable decompression buffer + z_stream per thread
    33→7. Flat way_refs array (eliminates per-way vector allocation)
    34→8. PbfBlock reuse across iterations (keeps vector capacity)
    35→9. Zero-copy tag storage (string table indices, not copies)
    36→10. posix_fadvise WILLNEED for page cache warming
    37→11. Single-pass tag extraction + early exit for irrelevant ways
    38→12. Bitset admin way lookup (replaces hash set)
    39→13. First-character switch for highway/tag comparisons
    40→
    41→### Data Processing
    42→14. Parallel continents (4 concurrent, largest-first by bbox area)
    43→15. Eliminated planet cell map rebuild (filter from sorted pairs)
    44→16. Parallel remap in filter_by_bbox
    45→17. Parallel mode writes (full/no-addresses/admin simultaneously)
    46→18. Parallel quality variants (3 concurrent)
    47→19. Sorted pair filtering (cache-friendly linear scan)
    48→20. Parallel chunked scanning per continent
    49→21. Parallel per-cell dedup
    50→22. Admin polygon sorting (largest-first for work-stealing)
    51→23. Worker threads = N-1
    52→24. Deterministic interp resolution (lexicographic tiebreaker)
    53→25. Sorted pair remap for continents (fixes street_entries bug)
    54→26. Precomputed continent bitmasks (1 S2 lookup per cell instead of 8)
    55→27. Bitset ID collection for continent filtering (replaces hash set)
    56→28. Per-thread bitsets to avoid race conditions
    57→29. MADV_HUGEPAGE on dense node index
    58→
    59→### Continent Boundaries
    60→30. Geofabrik .poly files for continent boundaries
    61→31. Coordinate-level polygon filtering (point-in-polygon per item)
    62→32. Any-node-inside way matching (matches Geofabrik behavior)
    63→
    64→### Quality Variants
    65→33. Never drop polygons — binary search for minimum epsilon preserving ≥4 vertices
    66→
    67→### Profiling
    68→34. Per-phase CPU/memory profiling (getrusage + /proc/self/statm)
    69→35. Per-thread read/decode/callback timing in streaming readers
    70→
    71→## Bug Fixes
    72→- Continent street_entries.bin was empty (fixed by sorted pair remap)
    73→- Deterministic interp resolution (old code had nondeterministic last-wins)
    74→- PBF way callback used return instead of continue (skipped 50% of ways)
    75→- Race condition in continent bitset (per-thread bitsets fix)
    76→- Polygons dropped during quality simplification (min-vertex enforcement)
    77→
    78→## Multi-Machine Results
    79→
    80→| Machine | Cores | RAM | Build Time |
    81→|---|---|---|---|
    82→| Node 3 (Newer CPU) | 32 | 174 GiB | **12m 22s** |
    83→| Node 2b (E5-2699A v4) | 80 (cgroup) | 192 GiB | ~22-27m (I/O bound) |
    84→
    85→Node 2b is slower despite more cores due to older CPU, slower storage, and
    86→NUMA effects on the 111 GiB dense index. The fadvise optimization helps
    87→significantly (72% faster reads) but the machine's I/O subsystem limits
    88→parallel pread throughput.
    89→


View recent git history


ddeebde feat: BUILD_MAX_IO env var to tune I/O concurrency
0798fb3 perf: I/O concurrency semaphore for streaming PBF reads
24860a2 Revert "perf: producer-consumer model for PBF streaming reads"
e7cfd60 Revert "fix: use mutex+deque for producer-consumer queue (lock-free version had races)"
cf73295 fix: use mutex+deque for producer-consumer queue (lock-free version had races)
3b23b6a perf: producer-consumer model for PBF streaming reads
3fb83b1 Revert "perf: compact node index — only store nodes referenced by ways"
28da658 perf: compact node index — only store nodes referenced by ways
a83a6c2 Revert "perf: paged dense index — only allocate memory for populated regions"
d8162f3 perf: paged dense index — only allocate memory for populated regions
4bc9816 revert: back to anonymous mmap for dense index
7a382a3 fix: use TMPDIR or cwd for dense index temp file (avoid tmpfs)
66e4af6 perf: file-backed mmap for dense node index (SSD paging vs swap)
2174c32 docs: comprehensive optimization log — 54m → 12m22s (77% faster, 35 optimizations)
9b013e7 restore: posix_fadvise WILLNEED (crash was hardware, not OOM)
8f3a4a1 revert: remove posix_fadvise WILLNEED (caused OOM on memory-constrained machines)
d64cf8d perf: posix_fadvise WILLNEED to preload PBF into page cache
9664a14 debug: add per-thread timing to streaming node/way readers
a646f5b perf: binary search blob classification (32 decompressions vs 50K)
61b3951 perf: skip blob classification + reusable pread/zlib buffers


    60→    uint64_t entry_count;
    61→    std::memcpy(&entry_count, ptr, sizeof(uint64_t)); ptr += sizeof(uint64_t);
    62→    map.reserve(entry_count);
    63→    for (uint64_t i = 0; i < entry_count; i++) {
    64→        if (ptr + sizeof(uint64_t) + sizeof(uint32_t) > end) return false;
    65→        uint64_t cell_id;
    66→        std::memcpy(&cell_id, ptr, sizeof(uint64_t)); ptr += sizeof(uint64_t);
    67→        uint32_t id_count;
    68→        std::memcpy(&id_count, ptr, sizeof(uint32_t)); ptr += sizeof(uint32_t);
    69→        if (ptr + id_count * sizeof(uint32_t) > end) return false;
    70→        std::vector<uint32_t> ids(id_count);
    71→        if (id_count > 0) { std::memcpy(ids.data(), ptr, id_count * sizeof(uint32_t)); ptr += id_count * sizeof(uint32_t); }
    72→        map[cell_id] = std::move(ids);
    73→    }
    74→    return true;
    75→}
    76→
    77→void serialize_cache(const ParsedData& data, const std::string& path) {
    78→    std::cerr << "Saving cache to " << path << "..." << std::endl;
    79→    std::ofstream f(path, std::ios::binary);
    80→    if (!f.is_open()) { std::cerr << "Error: could not open cache file for writing: " << path << std::endl; return; }
    81→    f.write(CACHE_MAGIC, 8);
    82→    f.write(reinterpret_cast<const char*>(&CACHE_VERSION), sizeof(uint32_t));
    83→    f.write(reinterpret_cast<const char*>(&CACHE_SECTION_COUNT), sizeof(uint32_t));
    84→    auto write_section = [&](SectionType type, const std::vector<char>& blob) {
    85→        uint32_t t = static_cast<uint32_t>(type);
    86→        uint64_t len = blob.size();
    87→        f.write(reinterpret_cast<const char*>(&t), sizeof(uint32_t));
    88→        f.write(reinterpret_cast<const char*>(&len), sizeof(uint64_t));
    89→        f.write(blob.data(), len);
    90→    };
    91→    { const auto& sp = data.string_pool.data();
    92→      std::vector<char> blob(sizeof(uint64_t) + sp.size());
    93→      uint64_t sz = sp.size();
    94→      std::memcpy(blob.data(), &sz, sizeof(uint64_t));
    95→      if (sz > 0) std::memcpy(blob.data() + sizeof(uint64_t), sp.data(), sz);
    96→      write_section(SectionType::STRING_POOL, blob); }
    97→    write_section(SectionType::WAYS, serialize_vector(data.ways));
    98→    write_section(SectionType::STREET_NODES, serialize_vector(data.street_nodes));
    99→    write_section(SectionType::CELL_TO_WAYS, serialize_cell_map(data.cell_to_ways));
   100→    write_section(SectionType::ADDR_POINTS, serialize_vector(data.addr_points));
   101→    write_section(SectionType::CELL_TO_ADDRS, serialize_cell_map(data.cell_to_addrs));
   102→    write_section(SectionType::INTERP_WAYS, serialize_vector(data.interp_ways));
   103→    write_section(SectionType::INTERP_NODES, serialize_vector(data.interp_nodes));
   104→    write_section(SectionType::CELL_TO_INTERPS, serialize_cell_map(data.cell_to_interps));
   105→    write_section(SectionType::ADMIN_POLYGONS, serialize_vector(data.admin_polygons));
   106→    write_section(SectionType::ADMIN_VERTICES, serialize_vector(data.admin_vertices));
   107→    write_section(SectionType::CELL_TO_ADMIN, serialize_cell_map(data.cell_to_admin));
   108→    std::cerr << "Cache saved." << std::endl;
   109→}
   110→
   111→bool deserialize_cache(ParsedData& data, const std::string& path) {
   112→    std::cerr << "Loading cache from " << path << "..." << std::endl;
   113→    std::ifstream f(path, std::ios::binary);
   114→    if (!f.is_open()) { std::cerr << "Error: could not open cache file: " << path << std::endl; return false; }
   115→    char magic[8]; f.read(magic, 8);
   116→    if (std::memcmp(magic, CACHE_MAGIC, 8) != 0) { std::cerr << "Error: invalid cache magic" << std::endl; return false; }
   117→    uint32_t version; f.read(reinterpret_cast<char*>(&version), sizeof(uint32_t));
   118→    if (version != CACHE_VERSION) { std::cerr << "Error: unsupported cache version " << version << std::endl; return false; }
   119→    uint32_t section_count; f.read(reinterpret_cast<char*>(&section_count), sizeof(uint32_t));
   120→    for (uint32_t s = 0; s < section_count; s++) {
   121→        uint32_t type; uint64_t length;
   122→        f.read(reinterpret_cast<char*>(&type), sizeof(uint32_t));
   123→        f.read(reinterpret_cast<char*>(&length), sizeof(uint64_t));
   124→        std::vector<char> blob(length); f.read(blob.data(), length);
   125→        switch (static_cast<SectionType>(type)) {
   126→        case SectionType::STRING_POOL: {
   127→            if (length < sizeof(uint64_t)) return false;
   128→            uint64_t sz; std::memcpy(&sz, blob.data(), sizeof(uint64_t));
   129→            auto& sp = data.string_pool.mutable_data(); sp.resize(sz);
   130→            if (sz > 0) std::memcpy(sp.data(), blob.data() + sizeof(uint64_t), sz);
   131→            break; }
   132→        case SectionType::WAYS: if (!deserialize_vector(blob.data(), length, data.ways)) return false; break;
   133→        case SectionType::STREET_NODES: if (!deserialize_vector(blob.data(), length, data.street_nodes)) return false; break;
   134→        case SectionType::CELL_TO_WAYS: if (!deserialize_cell_map(blob.data(), length, data.cell_to_ways)) return false; break;
   135→        case SectionType::ADDR_POINTS: if (!deserialize_vector(blob.data(), length, data.addr_points)) return false; break;
   136→        case SectionType::CELL_TO_ADDRS: if (!deserialize_cell_map(blob.data(), length, data.cell_to_addrs)) return false; break;
   137→        case SectionType::INTERP_WAYS: if (!deserialize_vector(blob.data(), length, data.interp_ways)) return false; break;
   138→        case SectionType::INTERP_NODES: if (!deserialize_vector(blob.data(), length, data.interp_nodes)) return false; break;
   139→        case SectionType::CELL_TO_INTERPS: if (!deserialize_cell_map(blob.data(), length, data.cell_to_interps)) return false; break;
   140→        case SectionType::ADMIN_POLYGONS: if (!deserialize_vector(blob.data(), length, data.admin_polygons)) return false; break;
   141→        case SectionType::ADMIN_VERTICES: if (!deserialize_vector(blob.data(), length, data.admin_vertices)) return false; break;
   142→        case SectionType::CELL_TO_ADMIN: if (!deserialize_cell_map(blob.data(), length, data.cell_to_admin)) return false; break;
   143→        default: std::cerr << "Warning: unknown cache section type " << type << ", skipping" << std::endl; break;
   144→        }
   145→    }
   146→    std::cerr << "Cache loaded: " << data.ways.size() << " ways, " << data.addr_points.size() << " addrs, "
   147→              << data.interp_ways.size() << " interps, " << data.admin_polygons.size() << " admin polygons" << std::endl;
   148→    return true;
   149→}
   150→


   560→    // The peak at (0.5, 1) should be kept
   561→    ASSERT_TRUE(keep[1]);
   562→    PASS();
   563→}
   564→
   565→// --- New tests: binary structs ---
   566→
   567→void test_way_header_size() {
   568→    TEST("WayHeader struct size");
   569→    // Ensure struct is packed as expected
   570→    ASSERT_TRUE(sizeof(WayHeader) > 0);
   571→    ASSERT_TRUE(sizeof(WayHeader) <= 16); // should be compact
   572→    PASS();
   573→}
   574→
   575→void test_admin_polygon_struct() {
   576→    TEST("AdminPolygon country_code encoding");
   577→    AdminPolygon ap{};
   578→    const char* cc = "US";
   579→    ap.country_code = static_cast<uint16_t>((cc[0] << 8) | cc[1]);
   580→    ASSERT_EQ(ap.country_code >> 8, (uint16_t)'U');
   581→    ASSERT_EQ(ap.country_code & 0xFF, (uint16_t)'S');
   582→    PASS();
   583→}
   584→
   585→void test_interior_flag() {
   586→    TEST("INTERIOR_FLAG and ID_MASK are complementary");
   587→    uint32_t id = 12345;
   588→    uint32_t flagged = id | INTERIOR_FLAG;
   589→    ASSERT_EQ(flagged & ID_MASK, id);


src/build_index.cpp:187:        std::string prev_strings = previous_build_path + "/strings.bin";
src/build_index.cpp:1620:            "admin_cells.bin", "admin_entries.bin", "strings.bin"
src/build_index.cpp:1623:            "admin_polygons.bin", "admin_vertices.bin"
src/build_index.cpp:1626:            "street_ways.bin", "street_nodes.bin", "street_entries.bin"
src/build_index.cpp:1629:            "addr_points.bin", "addr_entries.bin",
src/build_index.cpp:1630:            "interp_ways.bin", "interp_nodes.bin", "interp_entries.bin"
src/build_index.cpp:1632:        std::vector<std::string> geo_files = {"geo_cells.bin"};
src/cell_index.cpp:229:                    return write_entries_from_sorted(output_dir + "/street_entries.bin", sorted_geo_cells, data.sorted_way_cells);
src/cell_index.cpp:230:                return write_entries(output_dir + "/street_entries.bin", sorted_geo_cells, data.cell_to_ways);
src/cell_index.cpp:235:                        return write_entries_from_sorted(output_dir + "/addr_entries.bin", sorted_geo_cells, data.sorted_addr_cells);
src/cell_index.cpp:236:                    return write_entries(output_dir + "/addr_entries.bin", sorted_geo_cells, data.cell_to_addrs);
src/cell_index.cpp:240:                        return write_entries_from_sorted(output_dir + "/interp_entries.bin", sorted_geo_cells, data.sorted_interp_cells);
src/cell_index.cpp:241:                    return write_entries(output_dir + "/interp_entries.bin", sorted_geo_cells, data.cell_to_interps);
src/cell_index.cpp:277:            std::ofstream f(output_dir + "/geo_cells.bin", std::ios::binary);
src/cell_index.cpp:286:    log_phase("  Write: geo_cells.bin", _wt);
src/cell_index.cpp:288:        write_cell_index(output_dir + "/admin_cells.bin", output_dir + "/admin_entries.bin", data.cell_to_admin);
src/cell_index.cpp:295:            std::ofstream f(output_dir + "/street_ways.bin", std::ios::binary);
src/cell_index.cpp:299:            std::ofstream f(output_dir + "/street_nodes.bin", std::ios::binary);
src/cell_index.cpp:304:                std::ofstream f(output_dir + "/addr_points.bin", std::ios::binary);
src/cell_index.cpp:308:                std::ofstream f(output_dir + "/interp_ways.bin", std::ios::binary);
src/cell_index.cpp:312:                std::ofstream f(output_dir + "/interp_nodes.bin", std::ios::binary);
src/cell_index.cpp:318:    // before writing admin_polygons and strings.bin.
src/cell_index.cpp:362:        std::ofstream f(output_dir + "/admin_polygons.bin", std::ios::binary);
src/cell_index.cpp:366:        std::ofstream f(output_dir + "/admin_vertices.bin", std::ios::binary);
src/cell_index.cpp:371:            std::ofstream f(output_dir + "/strings.bin", std::ios::binary);
src/cell_index.cpp:373:            std::cerr << "strings.bin: " << compact_strings.size() << " bytes (compacted from "
src/cell_index.cpp:376:            std::ofstream f(output_dir + "/strings.bin", std::ios::binary);
src/cell_index.cpp:378:            std::cerr << "strings.bin: " << data.string_pool.data().size() << " bytes" << std::endl;
src/cell_index.cpp:458:        std::ofstream f(output_dir + "/admin_polygons.bin", std::ios::binary);
src/cell_index.cpp:462:        std::ofstream f(output_dir + "/admin_vertices.bin", std::ios::binary);
src/cell_index.cpp:467:    // Shared files (admin_cells.bin, admin_entries.bin, strings.bin)


  1610→        auto dir_size = [&](const std::string& dir, const std::vector<std::string>& files) -> int64_t {
  1611→            int64_t total = 0;
  1612→            for (const auto& f : files) {
  1613→                int64_t s = file_size(dir + "/" + f);
  1614→                if (s > 0) total += s;
  1615→            }
  1616→            return total;
  1617→        };
  1618→
  1619→        std::vector<std::string> admin_base_files = {
  1620→            "admin_cells.bin", "admin_entries.bin", "strings.bin"
  1621→        };
  1622→        std::vector<std::string> admin_quality_files = {
  1623→            "admin_polygons.bin", "admin_vertices.bin"
  1624→        };
  1625→        std::vector<std::string> street_files = {
  1626→            "street_ways.bin", "street_nodes.bin", "street_entries.bin"
  1627→        };
  1628→        std::vector<std::string> address_files = {
  1629→            "addr_points.bin", "addr_entries.bin",
  1630→            "interp_ways.bin", "interp_nodes.bin", "interp_entries.bin"
  1631→        };
  1632→        std::vector<std::string> geo_files = {"geo_cells.bin"};
  1633→
  1634→        // Build list of regions
  1635→        std::vector<std::pair<std::string, std::string>> regions; // (name, path)
  1636→        regions.push_back({"planet", output_dir});
  1637→        if (generate_continents) {
  1638→            for (size_t ci = 0; ci < kContinentCount; ci++) {
  1639→                regions.push_back({kContinents[ci].name,
  1640→                    output_dir + "/" + kContinents[ci].name});
  1641→            }
  1642→        }
  1643→
  1644→        // Quality level names
  1645→        auto quality_dir_name = [](double scale) -> std::string {
  1646→            if (scale == 0) return "uncapped";
  1647→            char buf[32]; snprintf(buf, sizeof(buf), "q%.4g", scale);
  1648→            return buf;
  1649→        };
  1650→
  1651→        std::ofstream mf(output_dir + "/manifest.json");
  1652→        mf << "{\n";
  1653→        mf << "  \"version\": 1,\n";
  1654→        mf << "  \"regions\": [\n";
  1655→
  1656→        for (size_t ri = 0; ri < regions.size(); ri++) {
  1657→            const auto& [rname, rpath] = regions[ri];
  1658→            mf << "    {\n";
  1659→            mf << "      \"name\": \"" << rname << "\",\n";
  1660→
  1661→            // Modes
  1662→            mf << "      \"modes\": {\n";
  1663→            if (multi_output) {
  1664→                // Full
  1665→                std::string full_dir = rpath + "/full";
  1666→                int64_t full_sz = dir_size(full_dir, geo_files) +
  1667→                    dir_size(full_dir, street_files) +
  1668→                    dir_size(full_dir, address_files) +
  1669→                    dir_size(full_dir, admin_base_files) +


13:ParsedData filter_by_bbox(const ParsedData& full, const ContinentBBox& bbox);
15:// Fast version using precomputed continent bitmasks (avoids cell_in_bbox per cell)
17:ParsedData filter_by_bbox_masked(const ParsedData& full, const ContinentBBox& bbox,
18:    uint8_t continent_bit,


     1→#pragma once
     2→
     3→#include "parsed_data.h"
     4→
     5→struct ContinentBBox {
     6→    const char* name;
     7→    double min_lat, max_lat, min_lng, max_lng;
     8→};
     9→
    10→extern const ContinentBBox kContinents[];
    11→extern const size_t kContinentCount;
    12→
    13→ParsedData filter_by_bbox(const ParsedData& full, const ContinentBBox& bbox);
    14→
    15→// Fast version using precomputed continent bitmasks (avoids cell_in_bbox per cell)
    16→// If polygon is provided, items are additionally tested at the coordinate level.
    17→ParsedData filter_by_bbox_masked(const ParsedData& full, const ContinentBBox& bbox,
    18→    uint8_t continent_bit,
    19→    const std::vector<uint8_t>& way_masks,
    20→    const std::vector<uint8_t>& addr_masks,
    21→    const std::vector<uint8_t>& interp_masks,
    22→    const std::vector<std::pair<double,double>>* polygon = nullptr);
    23→


I have now explored every relevant source file. Here is the complete design.

---

## Geocoder Index Patching System -- Design Plan

### 1. Architecture Overview

The system consists of three components:

1. **geocoder-canonicalize** -- normalizes an independently-built index directory into deterministic canonical byte-identical output
2. **geocoder-diff** -- compares two canonical directories and produces a `.gcpatch` file
3. **geocoder-patch** -- applies a `.gcpatch` to a canonical directory, producing a new canonical directory

The critical invariant is that `canonicalize(build_day_N)` always produces the same bytes for the same semantic data, regardless of which machine or thread count produced the build. This means `patch(canonical_day1, diff(canonical_day1, canonical_day2))` yields an output that is byte-identical to `canonical_day2`.

### 2. Canonical Ordering Rules

Canonicalization establishes a deterministic total ordering for every record type. This is the foundation that makes binary diffing work.

**Streets (street_ways.bin + street_nodes.bin):**
- Primary key: resolved street name (string at `name_id`)
- Secondary key: `node_count`
- Tertiary key: sorted list of grid coordinates `(to_grid(lat), to_grid(lng))` for all nodes, compared lexicographically
- This matches the existing `way_fingerprint` from `compare_indexes.cpp`

**Addresses (addr_points.bin):**
- Primary key: resolved street name (string at `street_id`)
- Secondary key: resolved house number (string at `housenumber_id`)
- Tertiary key: `(to_grid(lat), to_grid(lng))`
- This matches `addr_fingerprint`

**Interpolations (interp_ways.bin + interp_nodes.bin):**
- Primary key: resolved street name (string at `street_id`)
- Secondary key: `(start_number, end_number, interpolation)`
- Tertiary key: sorted node grid coordinates
- This matches `interp_fingerprint`

**Admin polygons (admin_polygons.bin + admin_vertices.bin):**
- Primary key: resolved name (string at `name_id`)
- Secondary key: `admin_level`
- Tertiary key: `country_code`
- Quaternary key: sorted vertex grid coordinates
- This matches `admin_fingerprint`

**Strings (strings.bin):**
- Sorted lexicographically by the string content itself
- Each string gets a new offset equal to its position in the sorted concatenation
- All records referencing strings are remapped after string sorting

**Cell indexes (geo_cells.bin, admin_cells.bin):**
- Already sorted by `cell_id` (this is how binary search works in the server)
- The entries files are already ordered by cell_id implicitly
- After record reordering, cell-to-item mappings must be regenerated with new item IDs

**Entry files (street_entries.bin, addr_entries.bin, interp_entries.bin, admin_entries.bin):**
- These are derived from the cell index and the item arrays
- Within each cell's entry list, item IDs are sorted ascending
- Fully deterministic given the canonical item ordering

**Node/vertex arrays (street_nodes.bin, interp_nodes.bin, admin_vertices.bin):**
- These are subarrays owned by their parent records
- They are laid out contiguously in the same order as their parent records
- When streets are reordered, their node slices follow, and `node_offset` is updated

### 3. Patch Binary Format (.gcpatch)

The patch file uses a section-based format inspired by the existing cache format:

```
Header (32 bytes):
  magic:         "GCPATCH\0"     (8 bytes)
  version:       u32             (= 1)
  flags:         u32             (reserved, 0)
  section_count: u32             (number of sections)
  old_checksum:  u64             (xxhash64 of concat of all 14 old files, for validation)

Section Table (section_count entries, each 16 bytes):
  type:          u32             (enum: see below)
  encoding:      u32             (0=raw_replace, 1=delta_run, 2=string_append)
  offset:        u64             (absolute byte offset in file)

Section Types (enum):
  0  STRINGS
  1  STREET_WAYS
  2  STREET_NODES
  3  ADDR_POINTS
  4  INTERP_WAYS
  5  INTERP_NODES
  6  ADMIN_POLYGONS
  7  ADMIN_VERTICES
  8  GEO_CELLS
  9  STREET_ENTRIES
  10 ADDR_ENTRIES
  11 INTERP_ENTRIES
  12 ADMIN_CELLS
  13 ADMIN_ENTRIES

Section Data (per section, depends on encoding):
```

**Encoding 0 -- raw_replace:** Full replacement of the file. Used as fallback for small files or when delta is larger than raw.
```
  uncompressed_size: u64
  compressed_size:   u64
  data:              zstd-compressed blob
```

**Encoding 1 -- delta_run:** Run-length encoded delta against the old canonical file. This is the primary encoding for large files with small changes. The format encodes a sequence of operations:
```
  old_size:          u64
  new_size:          u64
  op_count:          u32
  compressed_size:   u64
  data:              zstd-compressed ops blob

  Each op is one of:
    COPY   (tag=0): u8 tag, u32 old_offset_delta, u32 length
           Copy `length` bytes from old file at (prev_old_pos + old_offset_delta)
    INSERT (tag=1): u8 tag, u32 length, u8[length] data
           Insert `length` new bytes literally
```

The COPY/INSERT stream is compact because canonical ordering means that matching records in old and new tend to appear in the same relative order. With ~0.3% daily change, the stream is overwhelmingly COPY ops with small offset deltas and only a few INSERT ops for new records.

**Encoding 2 -- string_append:** Specialized for strings.bin which is append-only under canonical ordering when `--previous-build` stabilizes existing offsets.
```
  old_size:          u64   (expected size of old strings.bin)
  append_size:       u64
  append_data:       zstd-compressed new strings
```

However, under full canonical reordering, strings.bin is re-sorted, so encoding 1 (delta_run) would be used instead. The choice of encoding 2 is only valid when the diff tool detects that old strings.bin is a prefix of new strings.bin.

### 4. Diff Algorithm (geocoder-diff)

The diff tool operates at the semantic level, not raw bytes. It loads both canonical indexes, matches records, and produces the patch.

**Step 1: Load both indexes into memory**

Read all 14 files from both `old-dir` and `new-dir`. For an Europe dataset this means ~7 GiB each, so peak memory is ~14 GiB. Use `mmap` to reduce actual RSS -- pages are demand-loaded.

**Step 2: strings.bin handling**

Check if old strings.bin is a prefix of new strings.bin. If so, use encoding 2 (string_append). Otherwise, both are already in canonical sorted order; use delta_run encoding.

**Step 3: For each record-bearing file pair, compute delta_run**

For the fixed-size record files (street_ways.bin, addr_points.bin, interp_ways.bin, admin_polygons.bin):

1. Walk both old and new arrays in canonical order (they are already sorted canonically).
2. Use a two-pointer merge approach. For each record in new, check if the corresponding old record exists:
   - If bytes are identical at the same relative position, extend the current COPY run.
   - If old has the next matching record at a different position (deletion/insertion shifted things), emit a COPY with the correct offset delta.
   - If the record is new (not in old), emit an INSERT.
3. Emit COPY/INSERT ops.

For the node/vertex arrays (street_nodes.bin, interp_nodes.bin, admin_vertices.bin), these move in lockstep with their parent records. When a parent record is COPYed, its node slice is also COPYed from the same relative offset. When a parent record is INSERTed, its nodes are INSERTed.

For cell index files (geo_cells.bin, admin_cells.bin) and entry files, they are derived data. The diff tool regenerates them from the new record set and computes delta_run against old.

**Step 4: Compress each section with zstd**

Use zstd level 19 (high compression) since this is a batch pipeline and compression time does not matter. The high compression ratio is important -- 0.3% semantic change translates to maybe 2-5% byte-level change in the canonical files (due to offset shifts), and zstd at level 19 compresses repetitive copy instructions extremely well.

**Step 5: Choose encoding per section**

For each section, compute both raw_replace (zstd of new file) and delta_run sizes. Use whichever is smaller. For most sections with ~0.3% change, delta_run wins by a large margin.

### 5. Applicator Algorithm (geocoder-patch)

The applicator reads `current-dir` and `patch.gcpatch`, produces `new-dir`.

**Step 1: Validate**

- Check magic, version.
- Compute xxhash64 of all 14 files in `current-dir`, compare against `old_checksum` in header.
- If mismatch, abort with error (patch applied to wrong base).

**Step 2: Apply each section**

For each section in the patch:
- **raw_replace**: decompress zstd, write to output file.
- **delta_run**: mmap old file, replay COPY/INSERT ops sequentially into output buffer, write output file.
- **string_append**: copy old file, append decompressed new data.

**Step 3: Verify**

Optionally compute checksum of output (a `new_checksum` field could be added to the header for forward validation).

The applicator is intentionally simple -- it does not need to understand record semantics. It operates on raw bytes using the COPY/INSERT stream. This makes it fast (dominated by I/O) and impossible to produce non-deterministic output.

### 6. Canonical Ordering: Implementation Details

The canonicalize step (which can be a separate tool or integrated into the build pipeline) works as follows:

**Phase 1: Sort strings, build remap table**

1. Parse strings.bin into individual null-terminated strings.
2. Sort strings lexicographically.
3. Write new strings.bin with sorted strings concatenated.
4. Build `remap: old_offset -> new_offset` map.

**Phase 2: Sort each record type, remap string references**

For streets:
1. Read all WayHeader records plus their node slices.
2. Resolve name_id to string text via old strings.bin.
3. Sort by the canonical key defined above (name, node_count, sorted grid coords).
4. Write new street_ways.bin in sorted order, updating `name_id` using remap and `node_offset` to reflect the new sequential layout.
5. Write new street_nodes.bin with node slices laid out consecutively matching the new street order.

For addresses:
1. Read all AddrPoint records.
2. Sort by canonical key (street name, house number, grid coords).
3. Write new addr_points.bin with remapped `street_id` and `housenumber_id`.

For interpolations:
1. Read all InterpWay records plus their node slices.
2. Sort by canonical key.
3. Write new interp_ways.bin with remapped `street_id` and updated `node_offset`.
4. Write new interp_nodes.bin.

For admin polygons:
1. Read all AdminPolygon records plus their vertex slices.
2. Sort by canonical key.
3. Write new admin_polygons.bin with remapped `name_id` and updated `vertex_offset`.
4. Write new admin_vertices.bin.

**Phase 3: Rebuild cell indexes**

The cell-to-item mappings must be regenerated because item IDs (array indices) changed during sorting:

1. For each record type, rebuild the `cell_id -> [item_id]` map by re-computing S2 cell coverage.

Wait -- this requires S2 computation, which is expensive. Better approach: the canonical form should preserve the cell assignments from the original build. So we need to extract cell assignments from the original entry files and remap item IDs.

**Revised Phase 3: Extract and remap cell assignments**

1. Parse geo_cells.bin to get the list of `(cell_id, street_offset, addr_offset, interp_offset)` tuples.
2. For each cell, parse the corresponding entry in street_entries.bin to get the list of old street item IDs.
3. Using the `old_item_id -> new_item_id` permutation from Phase 2, remap all item IDs.
4. Sort item IDs within each cell.
5. Write new geo_cells.bin, street_entries.bin, addr_entries.bin, interp_entries.bin.
6. Do the same for admin_cells.bin / admin_entries.bin.

This avoids any S2 recomputation and is purely an index remapping operation.

### 7. Memory and Performance Estimates for Europe

Europe dataset approximate sizes (from the 7 GiB total and item counts given):

| File | Items | Record Size | File Size |
|------|-------|-------------|-----------|
| addr_points.bin | 86M | 16B | ~1.3 GiB |
| street_ways.bin | 19M | 9B (packed) | ~171 MiB |
| street_nodes.bin | ~150M nodes | 8B | ~1.1 GiB |
| admin_polygons.bin | 450K | ~24B | ~10 MiB |
| admin_vertices.bin | ~50M vertices | 8B | ~380 MiB |
| interp_ways.bin | ~2M | ~18B | ~34 MiB |
| interp_nodes.bin | ~10M nodes | 8B | ~76 MiB |
| strings.bin | ~10M unique strings | ~20B avg | ~200 MiB |
| geo_cells.bin | ~30M cells | 20B | ~570 MiB |
| street_entries.bin | variable | - | ~800 MiB |
| addr_entries.bin | variable | - | ~1.1 GiB |
| interp_entries.bin | variable | - | ~100 MiB |
| admin_cells.bin | ~2M cells | 12B | ~23 MiB |
| admin_entries.bin | variable | - | ~50 MiB |
| **Total** | | | **~6-7 GiB** |

**Diff tool memory:** ~14 GiB peak (both indexes mmap'd, but with demand paging the actual RSS is lower -- only touched pages are resident). The fingerprint computation for matching requires temporary string buffers but no additional large allocations since canonical files are already sorted and can be compared with a streaming two-pointer approach.

**Diff tool time:** Dominated by I/O. Reading 14 GiB from SSD at 2 GB/s takes ~7 seconds. The two-pointer merge is O(N) in record count. Zstd compression of the delta stream at level 19 takes ~10-20 seconds. Total: ~30-60 seconds.

**Patch tool memory:** Old file mmap'd (~7 GiB virtual, demand paged), output buffer for each file (largest single file is ~1.3 GiB for addr_points.bin). Peak RSS ~2-3 GiB.

**Patch tool time:** Dominated by I/O. Reading old files + writing new files: ~7 seconds. Zstd decompression of patch: <1 second. Total: ~10-15 seconds.

**Patch file size estimate for 0.3% daily change:**

With ~0.3% change rate across 86M addresses, 19M ways, 450K polygons:
- ~258K new/changed addresses, ~57K new/changed ways, ~1.3K new/changed polygons
- Changed record bytes: 258K * 16B + 57K * 9B + 1.3K * 24B + associated nodes + associated strings
- Raw delta: ~10-15 MiB of INSERT data
- COPY ops overhead: ~4-6 MiB (mostly just the op headers for long copy runs)
- After zstd compression: ~5-10 MiB

This falls within the 5-15 MiB target range.

### 8. Admin Polygon Handling

Admin polygons require special care because each polygon owns a variable-length vertex array in admin_vertices.bin.

During canonicalization:
1. Each AdminPolygon record is paired with its vertex slice `admin_vertices[vertex_offset..vertex_offset+vertex_count]`.
2. Polygons are sorted by `(name, admin_level, country_code, sorted_grid_vertices)`.
3. New admin_vertices.bin is written by concatenating vertex slices in the new polygon order.
4. Each polygon's `vertex_offset` is updated to point to the new position.

During diffing, admin_polygons.bin and admin_vertices.bin are treated as a linked pair. The delta_run for admin_vertices.bin naturally captures this because vertex slices for unchanged polygons remain at approximately the same relative position (they shift only by the cumulative size of inserted/deleted polygon vertex slices before them).

When an admin polygon is modified (e.g., boundary change), its old vertex slice is replaced entirely -- the COPY skips the old vertices and the INSERT provides the new ones.

### 9. File Structure

New files to create in `builder/tools/`:

```
builder/tools/
  geocoder_canonicalize.cpp   -- standalone canonicalize tool
  geocoder_diff.cpp           -- diff tool producing .gcpatch
  geocoder_patch.cpp          -- applicator tool
  patch_format.h              -- shared patch file format definitions
  canonical_order.h           -- shared canonical ordering / fingerprint functions
```

The `canonical_order.h` header would extract and generalize the fingerprinting logic from `test/compare_indexes.cpp` into reusable comparator functions that can be used as `std::sort` predicates.

The `patch_format.h` header defines the patch file magic, version, section types, encoding types, and read/write helpers -- similar to how `cache.cpp` defines its format.

### 10. CMakeLists.txt Changes

Three new executable targets need to be added:

```cmake
add_executable(geocoder-canonicalize tools/geocoder_canonicalize.cpp)
target_link_libraries(geocoder-canonicalize PRIVATE ZLIB::ZLIB Threads::Threads)

add_executable(geocoder-diff tools/geocoder_diff.cpp)
target_link_libraries(geocoder-diff PRIVATE ZLIB::ZLIB Threads::Threads)

add_executable(geocoder-patch tools/geocoder_patch.cpp)
target_link_libraries(geocoder-patch PRIVATE ZLIB::ZLIB Threads::Threads)
```

Note: zstd would need to be added as a dependency. Alternatively, zlib/deflate could be used since it is already a dependency, though zstd gives much better compression ratios for this use case.

### 11. Build Server Pipeline Integration

The daily build pipeline becomes:

```bash
# Step 1: Fresh build (already exists)
build-index output/ input.osm.pbf --previous-build prev_canonical/

# Step 2: Canonicalize
geocoder-canonicalize output/ canonical/

# Step 3: Diff against previous canonical
geocoder-diff prev_canonical/ canonical/ -o patches/patch_$(date +%Y%m%d).gcpatch

# Step 4: Archive canonical for next day
mv canonical/ prev_canonical/
```

Client-side update:
```bash
geocoder-patch current/ patch_20260327.gcpatch -o new/
mv new/ current/
```

### 12. Sequential Composition Guarantee

The key property: `patch(A, diff(A, B))` yields output byte-identical to `B`, and `patch(B, diff(B, C))` yields output byte-identical to `C`. This works because:

1. `diff(A, B)` encodes exactly the COPY/INSERT operations needed to transform canonical-A into canonical-B.
2. `patch` replays these operations mechanically.
3. The output of `patch` is byte-identical to `B` by construction.
4. Therefore `diff(B, C)` can be applied to the patch output.

There is no drift or accumulation of errors because the applicator does not make any semantic decisions -- it is a pure byte-level replay.

### 13. Edge Cases and Error Handling

- **Empty diff:** When old and new are identical, the patch file contains only COPY ops and compresses to near-zero. The patch still works correctly.
- **Full replacement:** When the diff is larger than the raw new file (e.g., major schema change), encoding 0 (raw_replace) is used. The patch is larger but still correct.
- **Checksum mismatch:** The applicator refuses to apply a patch if the base checksum does not match. This prevents applying patches out of order.
- **Interrupted application:** Since output goes to a new directory, an interrupted patch leaves the old directory intact. Atomic rename after verification provides crash safety.
- **String pool growth:** Under `--previous-build`, strings.bin is append-only (old offsets preserved). Under canonical reordering, it is fully re-sorted. The diff tool handles both cases.

### Critical Files for Implementation

- `/root/traccar-geocoder/builder/src/types.h` -- all binary record structs (WayHeader, AddrPoint, InterpWay, AdminPolygon, NodeCoord) that define the on-disk format
- `/root/traccar-geocoder/builder/test/compare_indexes.cpp` -- existing fingerprint/canonical ordering logic to extract into shared headers (way_fingerprint, addr_fingerprint, admin_fingerprint, interp_fingerprint, to_grid, read_structs)
- `/root/traccar-geocoder/builder/src/cell_index.cpp` -- write_entries, write_entries_from_sorted, write_cell_index functions needed for cell index regeneration in the canonicalize step
- `/root/traccar-geocoder/builder/src/string_pool.h` -- StringPool with load_previous / intern / offset-based referencing that the canonical reordering must remap
- `/root/traccar-geocoder/builder/CMakeLists.txt` -- build configuration to add the three new tool targets and the zstd dependency

Prompt:
Design a custom geocoder index patching system. Two tools:

1. `geocoder-diff <old-dir> <new-dir> -o patch.gcpatch` — compares two independently-built indexes
2. `geocoder-patch <current-dir> patch.gcpatch -o <new-dir>` — applies patch

Requirements:
- Daily builds are independent (no dependency on previous)
- Patches must be small (~5-15 MiB for 7 GiB Europe dataset with ~0.3% daily change)
- Sequential patching: day1 + patch12 → day2, then day2 + patch23 → day3

The key insight: the applicator always produces DETERMINISTIC canonical output. Same semantic data → same bytes. So patches compose correctly.

Build server pipeline:
1. Fresh build → output/
2. Canonicalize → canonical/
3. Diff(prev_canonical, new_canonical) → patch

## File Format (14 files)
- geo_cells.bin: sorted [cell_id:u64, street_off:u32, addr_off:u32, interp_off:u32] (20B)
- admin_cells.bin: sorted [cell_id:u64, off:u32] (12B)
- *_entries.bin: variable [count:u16][id:u32]*count
- street_ways.bin: [node_offset:u32, node_count:u8, name_id:u32] (9B packed)
- addr_points.bin: [lat:f32, lng:f32, hn_id:u32, street_id:u32] (16B)
- interp_ways.bin: [node_off:u32, count:u8, street_id:u32, start:u32, end:u32, type:u8] (18B+padding)
- admin_polygons.bin: [vert_off:u32, count:u32, name_id:u32, level:u8, area:f32, cc:u16] (+padding)
- street_nodes.bin, interp_nodes.bin, admin_vertices.bin: [lat:f32, lng:f32] (8B)
- strings.bin: null-terminated concatenated strings, referenced by byte offset

Existing fingerprinting (compare_indexes.cpp):
- way: "name|node_count|sorted_grid_coords"
- addr: "grid_lat,grid_lng|housenumber|street"
- admin: "name|admin_level|country|sorted_grid_vertices"

Design:
1. Patch binary format (sections, encoding)
2. Diff algorithm (matching, cell assignment extraction)
3. Applicator algorithm (rebuild each of 14 files deterministically)
4. Canonical ordering for each record type
5. Memory/performance estimates for Europe (86M addrs, 19M ways, 450K polygons)
6. Admin handling (polygons linked to vertex arrays)

Implementation in C++, builder/tools/. Can reuse types.h and string_pool.h.


ToolSearch

Question: select:ExitPlanMode


Question: select:ExitPlanMode