/root/traccar-geocoder/builder/src/cell_index.cpp:        std::vector<uint64_t> sorted_geo_cells;
/root/traccar-geocoder/builder/src/cell_index.cpp:            sorted_geo_cells.reserve(way_cells.size() + addr_cells.size() + interp_cells.size());
/root/traccar-geocoder/builder/src/cell_index.cpp:                       std::back_inserter(sorted_geo_cells));
/root/traccar-geocoder/builder/src/cell_index.cpp:                tmp.reserve(sorted_geo_cells.size() + interp_cells.size());
/root/traccar-geocoder/builder/src/cell_index.cpp:                std::merge(sorted_geo_cells.begin(), sorted_geo_cells.end(),
/root/traccar-geocoder/builder/src/cell_index.cpp:                sorted_geo_cells = std::move(tmp);
/root/traccar-geocoder/builder/src/cell_index.cpp:            sorted_geo_cells.erase(std::unique(sorted_geo_cells.begin(), sorted_geo_cells.end()),
/root/traccar-geocoder/builder/src/cell_index.cpp:                                    sorted_geo_cells.end());
/root/traccar-geocoder/builder/src/cell_index.cpp:                    return write_entries_from_sorted(output_dir + "/street_entries.bin", sorted_geo_cells, data.sorted_way_cells);
/root/traccar-geocoder/builder/src/cell_index.cpp:                return write_entries(output_dir + "/street_entries.bin", sorted_geo_cells, data.cell_to_ways);
/root/traccar-geocoder/builder/src/cell_index.cpp:                        return write_entries_from_sorted(output_dir + "/addr_entries.bin", sorted_geo_cells, data.sorted_addr_cells);
/root/traccar-geocoder/builder/src/cell_index.cpp:                    return write_entries(output_dir + "/addr_entries.bin", sorted_geo_cells, data.cell_to_addrs);
/root/traccar-geocoder/builder/src/cell_index.cpp:                        return write_entries_from_sorted(output_dir + "/interp_entries.bin", sorted_geo_cells, data.sorted_interp_cells);
/root/traccar-geocoder/builder/src/cell_index.cpp:                    return write_entries(output_dir + "/interp_entries.bin", sorted_geo_cells, data.cell_to_interps);
/root/traccar-geocoder/builder/src/cell_index.cpp:            size_t n = sorted_geo_cells.size();
/root/traccar-geocoder/builder/src/cell_index.cpp:                        memcpy(ptr, &sorted_geo_cells[i], sizeof(uint64_t)); ptr += sizeof(uint64_t);
/root/traccar-geocoder/builder/src/cell_index.cpp:            std::ofstream f(output_dir + "/geo_cells.bin", std::ios::binary);
/root/traccar-geocoder/builder/src/cell_index.cpp:        std::cerr << "geo index: " << sorted_geo_cells.size() << " cells ("
/root/traccar-geocoder/builder/src/cell_index.cpp:    log_phase("  Write: geo_cells.bin", _wt);
/root/traccar-geocoder/builder/src/cell_index.cpp:        write_cell_index(output_dir + "/admin_cells.bin", output_dir + "/admin_entries.bin", data.cell_to_admin);
/root/traccar-geocoder/builder/src/cell_index.cpp:    // Shared files (admin_cells.bin, admin_entries.bin, strings.bin)
/root/traccar-geocoder/builder/src/build_index.cpp:            "admin_cells.bin", "admin_entries.bin", "strings.bin"
/root/traccar-geocoder/builder/src/build_index.cpp:            "street_ways.bin", "street_nodes.bin", "street_entries.bin"
/root/traccar-geocoder/builder/src/build_index.cpp:        std::vector<std::string> geo_files = {"geo_cells.bin"};


     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→        return 1;
    62→    }
    63→
    64→    std::string output_dir = argv[1];
    65→    std::vector<std::string> input_files;
    66→    std::string save_cache_path;
    67→    std::string load_cache_path;
    68→    bool multi_output = false;
    69→    bool generate_continents = false;
    70→    IndexMode mode = IndexMode::Full;
    71→    SimplifyMode simplify_mode = SimplifyMode::MaxVertices;
    72→    double simplify_epsilon_override = 0; // 0 = use per-level defaults
    73→    bool multi_quality = false;
    74→    std::vector<double> quality_scales;
    75→
    76→    for (int i = 2; i < argc; i++) {
    77→        std::string arg = argv[i];
    78→        if (arg == "--street-level" && i + 1 < argc) {
    79→            kStreetCellLevel = std::atoi(argv[++i]);
    80→        } else if (arg == "--admin-level" && i + 1 < argc) {
    81→            kAdminCellLevel = std::atoi(argv[++i]);
    82→        } else if (arg == "--admin-only") {
    83→            mode = IndexMode::AdminOnly;
    84→        } else if (arg == "--no-addresses") {
    85→            mode = IndexMode::NoAddresses;
    86→        } else if (arg == "--max-admin-level" && i + 1 < argc) {
    87→            kMaxAdminLevel = std::atoi(argv[++i]);
    88→        } else if (arg == "--save-cache" && i + 1 < argc) {
    89→            save_cache_path = argv[++i];
    90→        } else if (arg == "--load-cache" && i + 1 < argc) {
    91→            load_cache_path = argv[++i];
    92→        } else if (arg == "--multi-output") {
    93→            multi_output = true;
    94→        } else if (arg == "--continents") {
    95→            generate_continents = true;
    96→        } else if (arg == "--simplify-epsilon") {
    97→            simplify_mode = SimplifyMode::ErrorBounded;
    98→            // Optional: next arg might be a number (fixed epsilon in meters)
    99→            if (i + 1 < argc) {
   100→                char* end;
   101→                double val = std::strtod(argv[i+1], &end);
   102→                if (*end == '\0' && val > 0) {
   103→                    simplify_epsilon_override = val;
   104→                    i++;
   105→                }
   106→            }
   107→        } else if (arg == "--epsilon-scale" && i + 1 < argc) {
   108→            kEpsilonScale = std::strtod(argv[++i], nullptr);
   109→        } else if (arg == "--multi-quality") {
   110→            multi_quality = true;
   111→            // Optional: next arg might be comma-separated scales
   112→            if (i + 1 < argc && argv[i+1][0] != '-') {
   113→                std::string vals = argv[++i];
   114→                size_t pos = 0;
   115→                while (pos < vals.size()) {
   116→                    size_t next = vals.find(',', pos);
   117→                    if (next == std::string::npos) next = vals.size();
   118→                    double v = std::strtod(vals.substr(pos, next - pos).c_str(), nullptr);
   119→                    quality_scales.push_back(v);
   120→                    pos = next + 1;
   121→                }
   122→            }
   123→            if (quality_scales.empty()) {
   124→                quality_scales = {0, 0.2, 0.5, 1.0, 1.5, 2.0, 2.5};
   125→            }
   126→            // Build with uncapped polygons — quality variants simplify at write time.
   127→            // We need full vertex detail to produce all quality levels accurately.
   128→            simplify_mode = SimplifyMode::ErrorBounded;
   129→            simplify_epsilon_override = 0.1; // effectively uncapped
   130→        } else if (arg == "--epsilon-levels" && i + 1 < argc) {
   131→            // Parse comma-separated values: L2,L3,L4,L5,L6,L7,L8
   132→            std::string vals = argv[++i];
   133→            int level = 2;
   134→            size_t pos = 0;
   135→            while (pos < vals.size() && level <= 8) {
   136→                size_t next = vals.find(',', pos);
   137→                if (next == std::string::npos) next = vals.size();
   138→                double v = std::strtod(vals.substr(pos, next - pos).c_str(), nullptr);
   139→                if (v > 0) kAdminEpsilonMeters[level] = v;
   140→                level++;
   141→                pos = next + 1;
   142→            }
   143→            // Also set levels 9-11 to same as level 8
   144→            for (int l = level; l <= 11; l++) kAdminEpsilonMeters[l] = kAdminEpsilonMeters[8];
   145→        } else if (arg == "--mode" && i + 1 < argc) {
   146→            std::string mode_str = argv[++i];
   147→            if (mode_str == "full") {
   148→                mode = IndexMode::Full;
   149→            } else if (mode_str == "no-addresses") {
   150→                mode = IndexMode::NoAddresses;


Bash

grep -n "size_t\|sizeof\|record\|entry" /root/traccar-geocoder/builder/src/cell_index.cpp | head -50


31:    size_t total_size = 0;
32:    for (auto& r : sorted_refs) total_size += sizeof(uint16_t) + r.ids->size() * sizeof(uint32_t);
37:    size_t ri = 0;
43:        uint16_t count = static_cast<uint16_t>(std::min(ids.size(), size_t(MAX_VERTEX_COUNT)));
45:                   reinterpret_cast<const char*>(&count) + sizeof(count));
47:                   reinterpret_cast<const char*>(ids.data()) + ids.size() * sizeof(uint32_t));
48:        current += sizeof(uint16_t) + ids.size() * sizeof(uint32_t);
69:    size_t cells_per_chunk = (sorted_cells.size() + nthreads - 1) / nthreads;
73:        size_t cell_start, cell_end;
80:        size_t cs = t * cells_per_chunk;
81:        size_t ce = std::min(cs + cells_per_chunk, sorted_cells.size());
90:            size_t pi = std::lower_bound(sorted_pairs.begin(), sorted_pairs.end(),
95:            for (size_t si = cs; si < ce && pi < sorted_pairs.size(); si++) {
101:                size_t start = pi;
103:                uint16_t count = static_cast<uint16_t>(std::min(pi - start, size_t(MAX_VERTEX_COUNT)));
104:                size_t entry_size = sizeof(uint16_t) + (pi - start) * sizeof(uint32_t);
105:                size_t buf_pos = chunk.buf.size();
106:                chunk.buf.resize(buf_pos + entry_size);
107:                memcpy(chunk.buf.data() + buf_pos, &count, sizeof(count));
108:                for (size_t k = start; k < pi; k++) {
109:                    memcpy(chunk.buf.data() + buf_pos + sizeof(uint16_t) + (k - start) * sizeof(uint32_t),
110:                           &sorted_pairs[k].item_id, sizeof(uint32_t));
112:                chunk.local_size += entry_size;
120:        for (size_t si = chunk.cell_start; si < chunk.cell_end; si++) {
146:          f.write(reinterpret_cast<const char*>(&cell_id), sizeof(cell_id));
147:          f.write(reinterpret_cast<const char*>(&current_offset), sizeof(current_offset));
148:          current_offset += sizeof(uint16_t) + ids.size() * sizeof(uint32_t);
153:          uint16_t count = static_cast<uint16_t>(std::min(ids.size(), size_t(MAX_VERTEX_COUNT)));
154:          f.write(reinterpret_cast<const char*>(&count), sizeof(count));
155:          f.write(reinterpret_cast<const char*>(ids.data()), ids.size() * sizeof(uint32_t));
172:                for (size_t i = 0; i < pairs.size(); ) {
249:        log_phase("  Write: entry files", _wt);
251:            size_t n = sorted_geo_cells.size();
252:            size_t row_size = sizeof(uint64_t) + 3 * sizeof(uint32_t);
259:            size_t chunk = (n + nthreads - 1) / nthreads;
262:                size_t start = t * chunk;
263:                size_t end = std::min(start + chunk, n);
267:                    for (size_t i = start; i < end; i++) {
268:                        memcpy(ptr, &sorted_geo_cells[i], sizeof(uint64_t)); ptr += sizeof(uint64_t);
269:                        memcpy(ptr, &street_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
270:                        memcpy(ptr, &addr_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
271:                        memcpy(ptr, &interp_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
296:            f.write(reinterpret_cast<const char*>(data.ways.data()), data.ways.size() * sizeof(WayHeader));
300:            f.write(reinterpret_cast<const char*>(data.street_nodes.data()), data.street_nodes.size() * sizeof(NodeCoord));
305:                f.write(reinterpret_cast<const char*>(data.addr_points.data()), data.addr_points.size() * sizeof(AddrPoint));
309:                f.write(reinterpret_cast<const char*>(data.interp_ways.data()), data.interp_ways.size() * sizeof(InterpWay));
313:                f.write(reinterpret_cast<const char*>(data.interp_nodes.data()), data.interp_nodes.size() * sizeof(NodeCoord));
346:            size_t len = std::strlen(old_sp.data() + off);
363:        f.write(reinterpret_cast<const char*>(admin_to_write.data()), admin_to_write.size() * sizeof(AdminPolygon));
367:        f.write(reinterpret_cast<const char*>(data.admin_vertices.data()), data.admin_vertices.size() * sizeof(NodeCoord));


cat > /tmp/sizeof_test.cpp << 'EOF'
#include <iostream>
#include <cstdint>

struct WayHeader {
    uint32_t node_offset;
    uint8_t node_count;
    uint32_t name_id;
};

struct AddrPoint {
    float lat;
    float lng;
    uint32_t housenumber_id;
    uint32_t street_id;
};

struct InterpWay {
    uint32_t node_offset;
    uint8_t node_count;
    uint32_t street_id;
    uint32_t start_number;
    uint32_t end_number;
    uint8_t interpolation;
};

struct AdminPolygon {
    uint32_t vertex_offset;
    uint32_t vertex_count;
    uint32_t name_id;
    uint8_t admin_level;
    float area;
    uint16_t country_code;
};

struct NodeCoord {
    float lat;
    float lng;
};

int main() {
    std::cout << "WayHeader: " << sizeof(WayHeader) << std::endl;
    std::cout << "AddrPoint: " << sizeof(AddrPoint) << std::endl;
    std::cout << "InterpWay: " << sizeof(InterpWay) << std::endl;
    std::cout << "AdminPolygon: " << sizeof(AdminPolygon) << std::endl;
    std::cout << "NodeCoord: " << sizeof(NodeCoord) << std::endl;
    return 0;
}
EOF
g++ /tmp/sizeof_test.cpp -o /tmp/sizeof_test && /tmp/sizeof_test


WayHeader: 12
AddrPoint: 16
InterpWay: 24
AdminPolygon: 24
NodeCoord: 8


grep -n "interior\|INTERIOR_FLAG\|ID_MASK" /root/traccar-geocoder/builder/src/*.cpp /root/traccar-geocoder/builder/src/*.h 2>/dev/null | head -20


/root/traccar-geocoder/builder/src/build_index.cpp:962:            // Remap poly_ids in cell_to_admin (preserving INTERIOR_FLAG)
/root/traccar-geocoder/builder/src/build_index.cpp:965:                    uint32_t flags = id & INTERIOR_FLAG;
/root/traccar-geocoder/builder/src/build_index.cpp:966:                    uint32_t old_id = id & ID_MASK;
/root/traccar-geocoder/builder/src/continent_filter.cpp:51:                                bool mask_interior = false) {
/root/traccar-geocoder/builder/src/continent_filter.cpp:57:                    ids.insert(mask_interior ? (id & ID_MASK) : id);
/root/traccar-geocoder/builder/src/continent_filter.cpp:67:                                   bool mask_interior = false) {
/root/traccar-geocoder/builder/src/continent_filter.cpp:97:                            local.insert(mask_interior ? (id & ID_MASK) : id);
/root/traccar-geocoder/builder/src/continent_filter.cpp:263:                uint32_t raw_id = handle_flags ? (id & ID_MASK) : id;
/root/traccar-geocoder/builder/src/continent_filter.cpp:264:                uint32_t flags = handle_flags ? (id & INTERIOR_FLAG) : 0;
/root/traccar-geocoder/builder/src/continent_filter.cpp:390:        // Admin still uses hash map (has interior flags)
/root/traccar-geocoder/builder/src/continent_filter.cpp:491:                                bool mask_interior = false) {
/root/traccar-geocoder/builder/src/continent_filter.cpp:496:                    ids.insert(mask_interior ? (id & ID_MASK) : id);
/root/traccar-geocoder/builder/src/continent_filter.cpp:636:                uint32_t raw_id = handle_flags ? (id & ID_MASK) : id;
/root/traccar-geocoder/builder/src/continent_filter.cpp:637:                uint32_t flags = handle_flags ? (id & INTERIOR_FLAG) : 0;
/root/traccar-geocoder/builder/src/s2_helpers.cpp:103:    S2CellUnion interior = coverer.GetInteriorCovering(polygon);
/root/traccar-geocoder/builder/src/s2_helpers.cpp:105:    std::unordered_set<uint64_t> interior_set;
/root/traccar-geocoder/builder/src/s2_helpers.cpp:106:    for (const auto& cell : interior.cell_ids()) {
/root/traccar-geocoder/builder/src/s2_helpers.cpp:111:                interior_set.insert(c.id());
/root/traccar-geocoder/builder/src/s2_helpers.cpp:113:            interior_set.insert(end.id());
/root/traccar-geocoder/builder/src/s2_helpers.cpp:115:            interior_set.insert(cell.parent(kAdminCellLevel).id());


     1→#include "s2_helpers.h"
     2→#include "geometry.h"
     3→
     4→#include <algorithm>
     5→#include <cmath>
     6→#include <iostream>
     7→#include <memory>
     8→
     9→#include <s2/s2latlng.h>
    10→#include <s2/s2region_coverer.h>
    11→#include <s2/s2polyline.h>
    12→#include <s2/s2polygon.h>
    13→#include <s2/s2loop.h>
    14→
    15→// --- S2 cell level globals ---
    16→
    17→int kStreetCellLevel = 17;
    18→int kAdminCellLevel = 10;
    19→int kMaxAdminLevel = 0;  // 0 means no filtering
    20→
    21→SimplifyMode kSimplifyMode = SimplifyMode::MaxVertices;
    22→double kSimplifyEpsilonOverride = 0;
    23→
    24→// --- cover_edge ---
    25→
    26→void cover_edge(double lat1, double lng1, double lat2, double lng2,
    27→                std::vector<S2CellId>& out) {
    28→    out.clear();
    29→
    30→    S2CellId c1 = S2CellId(S2LatLng::FromDegrees(lat1, lng1)).parent(kStreetCellLevel);
    31→    S2CellId c2 = S2CellId(S2LatLng::FromDegrees(lat2, lng2)).parent(kStreetCellLevel);
    32→
    33→    // Same cell — most common case for short edges
    34→    if (c1 == c2) {
    35→        out.push_back(c1);
    36→        return;
    37→    }
    38→
    39→    // Adjacent cells — check midpoint
    40→    S2CellId cm = S2CellId(S2LatLng::FromDegrees((lat1+lat2)/2, (lng1+lng2)/2)).parent(kStreetCellLevel);
    41→    if (cm == c1 || cm == c2) {
    42→        out.push_back(c1);
    43→        out.push_back(c2);
    44→        return;
    45→    }
    46→
    47→    // Longer edge — use S2RegionCoverer for correctness (handles geodesic curves)
    48→    thread_local S2RegionCoverer coverer = []() {
    49→        S2RegionCoverer::Options options;
    50→        options.set_fixed_level(kStreetCellLevel);
    51→        return S2RegionCoverer(options);
    52→    }();
    53→
    54→    S2Point p1 = S2LatLng::FromDegrees(lat1, lng1).ToPoint();
    55→    S2Point p2 = S2LatLng::FromDegrees(lat2, lng2).ToPoint();
    56→    if (p1 == p2) { out.push_back(c1); return; }
    57→
    58→    std::vector<S2Point> points = {p1, p2};
    59→    S2Polyline polyline(points);
    60→    S2CellUnion covering = coverer.GetCovering(polyline);
    61→    out = std::move(covering.cell_ids());
    62→}
    63→
    64→std::vector<S2CellId> cover_edge(double lat1, double lng1, double lat2, double lng2) {
    65→    std::vector<S2CellId> result;
    66→    cover_edge(lat1, lng1, lat2, lng2, result);
    67→    return result;
    68→}
    69→
    70→S2CellId point_to_cell(double lat, double lng) {
    71→    return S2CellId(S2LatLng::FromDegrees(lat, lng)).parent(kStreetCellLevel);
    72→}
    73→
    74→// --- cover_polygon ---
    75→
    76→std::vector<std::pair<S2CellId, bool>> cover_polygon(
    77→    const std::vector<std::pair<double,double>>& vertices) {
    78→    std::vector<S2Point> points;
    79→    points.reserve(vertices.size());
    80→    for (const auto& [lat, lng] : vertices) {
    81→        S2Point p = S2LatLng::FromDegrees(lat, lng).ToPoint();
    82→        if (!points.empty() && points.back() == p) continue;
    83→        points.push_back(p);
    84→    }
    85→    if (points.size() > 1 && points.front() == points.back()) {
    86→        points.pop_back();
    87→    }
    88→    if (points.size() < 3) return {};
    89→
    90→    S2Error error;
    91→    auto loop = std::make_unique<S2Loop>(points, S2Debug::DISABLE);
    92→    loop->Normalize();
    93→    if (loop->FindValidationError(&error)) return {};
    94→
    95→    S2Polygon polygon(std::move(loop));
    96→
    97→    S2RegionCoverer::Options options;
    98→    options.set_max_level(kAdminCellLevel);
    99→    options.set_max_cells(MAX_S2_CELLS_PER_POLY);
   100→
   101→    S2RegionCoverer coverer(options);
   102→    S2CellUnion covering = coverer.GetCovering(polygon);
   103→    S2CellUnion interior = coverer.GetInteriorCovering(polygon);
   104→
   105→    std::unordered_set<uint64_t> interior_set;
   106→    for (const auto& cell : interior.cell_ids()) {
   107→        if (cell.level() <= kAdminCellLevel) {
   108→            auto begin = cell.range_min().parent(kAdminCellLevel);
   109→            auto end = cell.range_max().parent(kAdminCellLevel);
   110→            for (auto c = begin; c != end; c = c.next()) {
   111→                interior_set.insert(c.id());
   112→            }
   113→            interior_set.insert(end.id());
   114→        } else {
   115→            interior_set.insert(cell.parent(kAdminCellLevel).id());
   116→        }
   117→    }
   118→
   119→    std::vector<std::pair<S2CellId, bool>> result;
   120→    for (const auto& cell : covering.cell_ids()) {
   121→        if (cell.level() <= kAdminCellLevel) {
   122→            auto begin = cell.range_min().parent(kAdminCellLevel);
   123→            auto end = cell.range_max().parent(kAdminCellLevel);
   124→            for (auto c = begin; c != end; c = c.next()) {
   125→                result.emplace_back(c, interior_set.count(c.id()) > 0);
   126→            }
   127→            result.emplace_back(end, interior_set.count(end.id()) > 0);
   128→        } else {
   129→            auto parent = cell.parent(kAdminCellLevel);
   130→            result.emplace_back(parent, interior_set.count(parent.id()) > 0);
   131→        }
   132→    }
   133→
   134→    std::sort(result.begin(), result.end(), [](const auto& a, const auto& b) {
   135→        return a.first < b.first;
   136→    });
   137→    auto it = result.begin();
   138→    for (auto curr = result.begin(); curr != result.end(); ) {
   139→        auto next = curr + 1;
   140→        bool is_interior = curr->second;
   141→        while (next != result.end() && next->first == curr->first) {
   142→            is_interior = is_interior || next->second;
   143→            ++next;
   144→        }
   145→        *it = {curr->first, is_interior};
   146→        ++it;
   147→        curr = next;
   148→    }
   149→    result.erase(it, result.end());
   150→    return result;


   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 {


cat > /tmp/detailed_struct.cpp << 'EOF'
#include <iostream>
#include <cstdint>
#include <iomanip>

#define offset_of(T, member) __builtin_offsetof(T, member)

struct WayHeader {
    uint32_t node_offset;
    uint8_t node_count;
    uint32_t name_id;
};

struct InterpWay {
    uint32_t node_offset;
    uint8_t node_count;
    uint32_t street_id;
    uint32_t start_number;
    uint32_t end_number;
    uint8_t interpolation;
};

struct AdminPolygon {
    uint32_t vertex_offset;
    uint32_t vertex_count;
    uint32_t name_id;
    uint8_t admin_level;
    float area;
    uint16_t country_code;
};

int main() {
    std::cout << std::hex << std::setfill('0');

    std::cout << "WayHeader offsets:" << std::endl;
    std::cout << "  node_offset @ 0x" << offset_of(WayHeader, node_offset) << std::endl;
    std::cout << "  node_count @ 0x" << offset_of(WayHeader, node_count) << std::endl;
    std::cout << "  name_id @ 0x" << offset_of(WayHeader, name_id) << std::endl;
    std::cout << "  sizeof = " << sizeof(WayHeader) << std::endl;

    std::cout << "InterpWay offsets:" << std::endl;
    std::cout << "  node_offset @ 0x" << offset_of(InterpWay, node_offset) << std::endl;
    std::cout << "  node_count @ 0x" << offset_of(InterpWay, node_count) << std::endl;
    std::cout << "  street_id @ 0x" << offset_of(InterpWay, street_id) << std::endl;
    std::cout << "  start_number @ 0x" << offset_of(InterpWay, start_number) << std::endl;
    std::cout << "  end_number @ 0x" << offset_of(InterpWay, end_number) << std::endl;
    std::cout << "  interpolation @ 0x" << offset_of(InterpWay, interpolation) << std::endl;
    std::cout << "  sizeof = " << sizeof(InterpWay) << std::endl;

    std::cout << "AdminPolygon offsets:" << std::endl;
    std::cout << "  vertex_offset @ 0x" << offset_of(AdminPolygon, vertex_offset) << std::endl;
    std::cout << "  vertex_count @ 0x" << offset_of(AdminPolygon, vertex_count) << std::endl;
    std::cout << "  name_id @ 0x" << offset_of(AdminPolygon, name_id) << std::endl;
    std::cout << "  admin_level @ 0x" << offset_of(AdminPolygon, admin_level) << std::endl;
    std::cout << "  area @ 0x" << offset_of(AdminPolygon, area) << std::endl;
    std::cout << "  country_code @ 0x" << offset_of(AdminPolygon, country_code) << std::endl;
    std::cout << "  sizeof = " << sizeof(AdminPolygon) << std::endl;

    return 0;
}
EOF
g++ /tmp/detailed_struct.cpp -o /tmp/detailed_struct && /tmp/detailed_struct


WayHeader offsets:
  node_offset @ 0x0
  node_count @ 0x4
  name_id @ 0x8
  sizeof = c
InterpWay offsets:
  node_offset @ 0x0
  node_count @ 0x4
  street_id @ 0x8
  start_number @ 0xc
  end_number @ 0x10
  interpolation @ 0x14
  sizeof = 18
AdminPolygon offsets:
  vertex_offset @ 0x0
  vertex_count @ 0x4
  name_id @ 0x8
  admin_level @ 0xc
  area @ 0x10
  country_code @ 0x14
  sizeof = 18


Now I have all the information needed. Let me create a comprehensive analysis document:

## Geocoder Index File Format Analysis

Based on a thorough examination of the builder code (`/root/traccar-geocoder/builder/src/cell_index.cpp`) and the server reader code (`/root/traccar-geocoder/server/src/main.rs`), here's a complete specification of each binary file format:

---

### Overview of File Organization

The geocoder system uses a **two-level indexing structure**:
1. **Cell Index Files** - Map S2 cell IDs to entry offsets (binary search lookup)
2. **Entry Files** - Variable-length arrays of IDs within each cell
3. **Data Files** - Fixed-size records indexed by ID

Data references strings via **32-bit byte offsets** into a **null-terminated string pool**.

---

### File Specifications

#### 1. **geo_cells.bin** - Merged Geographic Cell Index
**Purpose**: Maps S2 cells (level 17) to street, address, and interpolation data offsets

**Structure**: Fixed-size records, sorted by cell_id
- **Record size**: 20 bytes
- **Entry layout**:
  - `uint64_t cell_id` (8 bytes) - S2 cell ID at configured street level
  - `uint32_t street_offset` (4 bytes) - Offset into `street_entries.bin` (NO_DATA = 0xFFFFFFFF)
  - `uint32_t addr_offset` (4 bytes) - Offset into `addr_entries.bin`
  - `uint32_t interp_offset` (4 bytes) - Offset into `interp_entries.bin`

**Lookup method**: Binary search on cell_id (comments in server confirm 20 bytes/entry)

**Ordering**: Sorted by cell_id in ascending order

**Data references**: String offsets (name_id fields) from all streets/addresses

---

#### 2. **street_entries.bin** - Street IDs by Cell
**Purpose**: Lists street IDs contained in each geographic cell

**Structure**: Variable-length entries, referenced by byte offset from geo_cells.bin
- **Entry header** (2 bytes):
  - `uint16_t count` - Number of street IDs in this cell
- **Entry data** (variable, 4 bytes per ID):
  - `uint32_t[count]` - Array of street way IDs (indices into street_ways.bin)

**Record size**: Variable (2 + count*4 bytes)

**Ordering**: IDs within each cell are deduplicated and sorted

**Data references**:
- Links to `street_ways.bin` via numeric ID
- No inter-cell ordering; each cell's list is independent

---

#### 3. **street_ways.bin** - Street Way Metadata
**Purpose**: Metadata for each street way (segment)

**Structure**: Fixed-size records, indexed sequentially (0-based)
- **Record size**: 12 bytes (9 bytes data + 3 bytes padding for alignment)
- **Record layout**:
  - `uint32_t node_offset` - Starting index into `street_nodes.bin`
  - `uint8_t node_count` - Number of nodes in this way (max 255)
  - `uint32_t name_id` - String pool offset to street name (u32, read after uint8_t with padding)

**Record size**: 12 bytes (verified via sizeof, includes struct packing)

**Ordering**: Records are indexed sequentially by way ID

**Data references**:
- String offset (`name_id`) for street name lookup
- Node indices into `street_nodes.bin`

---

#### 4. **street_nodes.bin** - Street Coordinates
**Purpose**: Latitude/longitude points for street ways

**Structure**: Fixed-size coordinate records, indexed sequentially
- **Record size**: 8 bytes
- **Record layout**:
  - `float lat` (4 bytes)
  - `float lng` (4 bytes)

**Record size**: 8 bytes

**Ordering**: Nodes are grouped by way; way headers point to start index

**Data references**: None; pure geometry data

---

#### 5. **addr_entries.bin** - Address Point IDs by Cell
**Purpose**: Lists address point IDs contained in each geographic cell

**Structure**: Identical format to street_entries.bin
- **Entry header** (2 bytes):
  - `uint16_t count` - Number of address points in this cell
- **Entry data** (variable, 4 bytes per ID):
  - `uint32_t[count]` - Array of address point IDs (indices into addr_points.bin)

**Record size**: Variable (2 + count*4 bytes)

**Ordering**: IDs within each cell are deduplicated and sorted

**Data references**:
- Links to `addr_points.bin` via numeric ID
- Referenced from `geo_cells.bin`

---

#### 6. **addr_points.bin** - Address Points
**Purpose**: Individual address locations with house numbers and streets

**Structure**: Fixed-size coordinate + metadata records, indexed sequentially
- **Record size**: 16 bytes
- **Record layout**:
  - `float lat` (4 bytes)
  - `float lng` (4 bytes)
  - `uint32_t housenumber_id` - String pool offset to house number
  - `uint32_t street_id` - String pool offset to street name

**Record size**: 16 bytes

**Ordering**: Records are indexed sequentially by address ID

**Data references**:
- Two string offsets (housenumber, street)
- No geometric hierarchy; only location coordinates

---

#### 7. **interp_entries.bin** - Interpolation Way IDs by Cell
**Purpose**: Lists interpolation ways (address ranges) in each cell

**Structure**: Identical format to street_entries.bin
- **Entry header** (2 bytes):
  - `uint16_t count` - Number of interpolation ways in this cell
- **Entry data** (variable, 4 bytes per ID):
  - `uint32_t[count]` - Array of interpolation way IDs (indices into interp_ways.bin)

**Record size**: Variable (2 + count*4 bytes)

**Ordering**: IDs within each cell are deduplicated and sorted

**Data references**:
- Links to `interp_ways.bin` via numeric ID
- Referenced from `geo_cells.bin`

---

#### 8. **interp_ways.bin** - Interpolation Way Metadata
**Purpose**: Address ranges (start..end) mapped to street geometry

**Structure**: Fixed-size records, indexed sequentially (0-based)
- **Record size**: 24 bytes (14 bytes data + 10 bytes padding/packing)
- **Record layout**:
  - `uint32_t node_offset` - Starting index into `interp_nodes.bin`
  - `uint8_t node_count` - Number of nodes defining the street line
  - `uint32_t street_id` - String pool offset to street name
  - `uint32_t start_number` - First house number in range
  - `uint32_t end_number` - Last house number in range
  - `uint8_t interpolation` - Interpolation type (1/2 = odd/even, 0 = all)

**Record size**: 24 bytes (struct packing inserts 2 bytes padding after node_count, 6 bytes after interpolation)

**Ordering**: Records indexed sequentially by interpolation way ID

**Data references**:
- String offset (`street_id`) for street name
- Node indices into `interp_nodes.bin`

---

#### 9. **interp_nodes.bin** - Interpolation Coordinates
**Purpose**: Latitude/longitude points defining interpolation way geometry

**Structure**: Fixed-size coordinate records, indexed sequentially
- **Record size**: 8 bytes
- **Record layout**:
  - `float lat` (4 bytes)
  - `float lng` (4 bytes)

**Record size**: 8 bytes

**Ordering**: Nodes grouped by interpolation way; way headers point to start index

**Data references**: None; pure geometry data

---

#### 10. **admin_cells.bin** - Admin Boundary Cell Index
**Purpose**: Maps S2 cells (level 10) to administrative boundary entries

**Structure**: Fixed-size records, sorted by cell_id
- **Record size**: 12 bytes
- **Entry layout**:
  - `uint64_t cell_id` (8 bytes) - S2 cell ID at configured admin level
  - `uint32_t entry_offset` (4 bytes) - Offset into `admin_entries.bin` (NO_DATA = 0xFFFFFFFF)

**Lookup method**: Binary search on cell_id

**Ordering**: Sorted by cell_id in ascending order

**Data references**: Offsets into admin_entries.bin

---

#### 11. **admin_entries.bin** - Admin Polygon IDs by Cell
**Purpose**: Lists administrative polygons that cover/contain each cell

**Structure**: Variable-length entries, referenced by byte offset from admin_cells.bin
- **Entry header** (2 bytes):
  - `uint16_t count` - Number of polygon IDs in this cell
- **Entry data** (variable, 4 bytes per ID):
  - `uint32_t[count]` - Array of polygon IDs with flags:
    - **Bit 31** (0x80000000): INTERIOR_FLAG - Set if cell is fully interior to polygon
    - **Bits 0-30** (0x7FFFFFFF): ID_MASK - Actual polygon index into admin_polygons.bin

**Record size**: Variable (2 + count*4 bytes)

**Ordering**: IDs within entry are sorted and deduplicated

**Data references**:
- Links to `admin_polygons.bin` via masked ID
- INTERIOR_FLAG optimization: interior cells skip point-in-polygon tests

---

#### 12. **admin_polygons.bin** - Administrative Boundaries
**Purpose**: Metadata for administrative boundary polygons

**Structure**: Fixed-size records, indexed sequentially (0-based)
- **Record size**: 24 bytes
- **Record layout**:
  - `uint32_t vertex_offset` - Starting index into `admin_vertices.bin`
  - `uint32_t vertex_count` - Number of vertices defining boundary
  - `uint32_t name_id` - String pool offset to administrative name
  - `uint8_t admin_level` - OSM admin level (2=country, 4=state, 6=county, 8=city, 11=postcode)
  - `float area` - Polygon area (for smallest-first selection)
  - `uint16_t country_code` - Encoded 2-letter country code (big-endian: (code[0]<<8) | code[1])

**Record size**: 24 bytes (16 bytes data + 8 bytes packing)

**Ordering**: Records indexed sequentially by polygon ID

**Data references**:
- String offset (`name_id`) for boundary name
- Vertex indices into `admin_vertices.bin`
- Country code encoded in uint16_t (ASCII bytes packed)

---

#### 13. **admin_vertices.bin** - Administrative Boundary Vertices
**Purpose**: Latitude/longitude vertices for polygon boundaries

**Structure**: Fixed-size coordinate records, indexed sequentially
- **Record size**: 8 bytes
- **Record layout**:
  - `float lat` (4 bytes)
  - `float lng` (4 bytes)

**Record size**: 8 bytes

**Ordering**: Vertices grouped by polygon; headers point to start index

**Data references**: None; pure geometry data

---

#### 14. **strings.bin** - String Pool
**Purpose**: Centralized storage for all variable-length strings

**Structure**: Null-terminated strings concatenated sequentially
- **Format**: Each string followed by `\0` terminator
- **Encoding**: UTF-8
- **References**: Identified by **byte offset** (uint32_t)

**Example layout**:
```
[Offset]  [Content]
0         "Main Street\0"
12        "New York\0"
21        "USA\0"
25        ...
```

**Key characteristics**:
- **Ordering**: No specific order; offsets are absolute byte positions
- **String types referenced**:
  - Street names (from ways, addresses, interpolation)
  - House numbers (from addresses)
  - Administrative names (from polygons)
- **Compaction**: When writing partial indexes (AdminOnly, NoAddresses), unused strings are removed and offsets remapped
- **Lookup method**: Direct pointer arithmetic; find null terminator by scanning

---

### Cross-File References and Dependencies

**Reference graph**:
```
geo_cells.bin
  ├─> street_entries.bin -> street_ways.bin -> street_nodes.bin
  │                      -> strings.bin (name_id)
  │
  ├─> addr_entries.bin -> addr_points.bin -> strings.bin (housenumber_id, street_id)
  │
  └─> interp_entries.bin -> interp_ways.bin -> interp_nodes.bin
                          -> strings.bin (street_id)

admin_cells.bin
  └─> admin_entries.bin -> admin_polygons.bin -> admin_vertices.bin
                                             -> strings.bin (name_id)
```

### Constants and Special Values

- **NO_DATA**: `0xFFFFFFFF` - Indicates no data for a cell
- **INTERIOR_FLAG**: `0x80000000` - Admin polygon ID flag (interior cell)
- **ID_MASK**: `0x7FFFFFFF` - Mask to extract polygon ID from flagged value
- **MAX_VERTEX_COUNT**: `0x7FFFFFFF` - Never actually reached; administrative limit

### Lookup Process Summary

**Geographic Query (street, address, interpolation)**:
1. Compute S2 cell ID at street level from (lat, lng)
2. Binary search in `geo_cells.bin` for cell_id (20 bytes/entry)
3. Extract three offsets (street, addr, interp)
4. For each offset, if not NO_DATA:
   - Seek to offset in corresponding `*_entries.bin`
   - Read uint16_t count
   - Read count × uint32_t IDs
   - For each ID, index into corresponding data file (ways, points, interps)
   - Dereference pointers to nodes/coordinates and strings

**Admin Query (country, state, city, postcode)**:
1. Compute S2 cell ID at admin level from (lat, lng)
2. Binary search in `admin_cells.bin` for cell_id (12 bytes/entry)
3. Extract entry offset
4. Read uint16_t count from admin_entries.bin at offset
5. Read count × uint32_t IDs (with INTERIOR_FLAG)
6. For each ID (masked), index into admin_polygons.bin
7. If INTERIOR_FLAG set, point is inside polygon; skip point-in-polygon test
8. Otherwise, perform ray-casting test using vertices from admin_vertices.bin
9. Select smallest-area polygon per admin level

---

### Design Implications for Incremental Updates

**Opportunities for Smart Updates**:

1. **Geo Data** (streets/addresses/interpolation):
   - `geo_cells.bin` references offsets in `*_entries.bin` - **MUST rebuild if cell contents change**
   - `street_ways.bin`, `addr_points.bin`, `interp_ways.bin` are **indexed by ID**:
     - Appending new records is safe if IDs are sequential
     - Updating existing records at same indices is safe (same size)
     - **Challenge**: Entry files store variable-length ID lists - hard to incrementally update
   - `*_nodes.bin` files are concatenated arrays - hard to incrementally update without remapping offsets

2. **Admin Data** (boundaries):
   - `admin_cells.bin` is a cell index - can rebuild independently
   - `admin_polygons.bin` is indexed by ID - can append safely
   - `admin_vertices.bin` is concatenated - requires offset remapping if changed

3. **Strings**:
   - String pool is offset-based - **any insertion requires remapping all offsets**
   - Could maintain separate string pools per category (streets, addresses, etc.)
   - String compaction happens at write time

4. **Binary Search Indexes** (geo_cells, admin_cells):
   - Must be kept sorted
   - Must cover all active cells
   - Can be completely rebuilt if cell set changes

**Challenges**:
- Entry files use variable-length encoding (1 uint16_t + N*uint32_t per cell)
- Node offsets are absolute, not relative - cascading updates required
- String offsets are global - adding/removing strings cascades to all data files
- No version markers or checksums for integrity validation

---

This specification should provide a complete foundation for designing an incremental update system. The key insight is that **fixed-size data files can be updated incrementally, but variable-length entry files and global string pools require more sophisticated coordination**.

Prompt:
Explore the geocoder index file formats. I need to understand what each binary file contains and how it's structured. Look at:
1. builder/src/cell_index.cpp - how entries, cells, nodes, ways, vertices, polygons, strings, etc. are written
2. Any header/struct definitions that define the binary layout of each file type
3. The server/runtime code that reads these files (likely in src/ directory) to understand the format

For each file type (geo_cells.bin, street_entries.bin, street_nodes.bin, street_ways.bin, addr_entries.bin, addr_points.bin, admin_cells.bin, admin_entries.bin, admin_polygons.bin, admin_vertices.bin, interp_entries.bin, interp_nodes.bin, interp_ways.bin, strings.bin), tell me:
- What data it contains
- The record size (fixed or variable)
- How records are ordered/indexed
- Whether records reference other files (e.g. string offsets, cell IDs)

Be very thorough - I need this to design a smart incremental update system.


Warning: Permanently added '100.113.88.121' (ED25519) to the list of known hosts.
total 1538192
drwxrwxr-x 2 michtest michtest      4096 Mar 26 22:55 .
drwxrwxr-x 3 michtest michtest      4096 Mar 26 22:38 ..
-rw-rw-r-- 1 michtest michtest 298199580 Mar 26 22:38 addr_entries.bin.patch
-rw-rw-r-- 1 michtest michtest 926437680 Mar 26 22:38 addr_points.bin.patch
-rw-rw-r-- 1 michtest michtest    469439 Mar 26 22:38 admin_cells.bin.patch
-rw-rw-r-- 1 michtest michtest   2561303 Mar 26 22:38 admin_entries.bin.patch
-rw-rw-r-- 1 michtest michtest   4486815 Mar 26 22:38 admin_polygons.bin.patch
-rw-rw-r-- 1 michtest michtest   3382409 Mar 26 22:38 admin_vertices.bin.patch
-rw------- 1 michtest michtest         0 Mar 26 22:55 geo_cells.bin.patch
-rw-rw-r-- 1 michtest michtest     61727 Mar 26 22:38 interp_entries.bin.patch
-rw-rw-r-- 1 michtest michtest     18870 Mar 26 22:38 interp_nodes.bin.patch
-rw-rw-r-- 1 michtest michtest     92384 Mar 26 22:38 interp_ways.bin.patch
-rw-rw-r-- 1 michtest michtest 175546644 Mar 26 22:38 street_entries.bin.patch
-rw-rw-r-- 1 michtest michtest   6768640 Mar 26 22:38 street_nodes.bin.patch
-rw-rw-r-- 1 michtest michtest 123538341 Mar 26 22:38 street_ways.bin.patch
-rw-rw-r-- 1 michtest michtest  33493204 Mar 26 22:38 strings.bin.patch
===SIZES===
addr_entries.bin: old=394555152 new=395005236 patch=298199580 ratio=75.4%
addr_points.bin: old=1374269280 new=1375841776 patch=926437680 ratio=67.3%
admin_cells.bin: old=1693932 new=1693932 patch=469439 ratio=27.7%
admin_entries.bin: old=6430350 new=6425798 patch=2561303 ratio=39.8%
admin_polygons.bin: old=10821768 new=10820736 patch=4486815 ratio=41.4%
admin_vertices.bin: old=1378942528 new=1379176912 patch=3382409 ratio=.2%
geo_cells.bin: old=1673519400 new=1674295440 patch=0 ratio=0%
interp_entries.bin: old=159512 new=159386 patch=61727 ratio=38.7%
interp_nodes.bin: old=726304 new=726176 patch=18870 ratio=2.5%
interp_ways.bin: old=335712 new=335424 patch=92384 ratio=27.5%
street_entries.bin: old=586303506 new=586571766 patch=175546644 ratio=29.9%
street_nodes.bin: old=1453821288 new=1454966400 patch=6768640 ratio=.4%
street_ways.bin: old=233429760 new=233626176 patch=123538341 ratio=52.8%
strings.bin: old=82546573 new=82573397 patch=33493204 ratio=40.5%


The numbers tell a clear story. Let me analyze the zstd patch results:

**Great (< 5%):** admin_vertices (0.2%), street_nodes (0.4%), interp_nodes (2.5%)
**OK (25-45%):** admin_cells, street_entries, interp_*, admin_entries, strings, admin_polygons
**Bad (50%+):** street_ways (52.8%), addr_points (67.3%), addr_entries (75.4%)
**Broken:** geo_cells.bin (0 bytes — patch generation failed)

The user is right — zstd binary patching doesn't handle this well for the index-heavy files. The root cause is clear from the format analysis: **the index uses sequential positional IDs and absolute byte offsets**. Adding one street shifts every subsequent way ID, which cascades through entries → cells. Even identical data has completely different byte representations.

Let me investigate the geo_cells failure and then design a proper system.


sshpass -p 'elon-is-insane' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ProxyCommand="tailscale nc %h %p" michtest@100.113.88.121 'stat -c%s ~/test-europe-old/full/geo_cells.bin ~/test-europe-new/full/geo_cells.bin; echo "---"; zstd --version'

Check geo_cells sizes and zstd version


Warning: Permanently added '100.113.88.121' (ED25519) to the list of known hosts.
1673519400
1674295440
---
*** Zstandard CLI (64-bit) v1.5.7, by Yann Collet ***


sshpass -p 'elon-is-insane' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ProxyCommand="tailscale nc %h %p" michtest@100.113.88.121 'zstd --patch-from=$HOME/test-europe-old/full/geo_cells.bin $HOME/test-europe-new/full/geo_cells.bin -o /tmp/geo_cells.bin.patch -f 2>&1; stat -c%s /tmp/geo_cells.bin.patch 2>/dev/null'

Retry geo_cells patch generation


Warning: Permanently added '100.113.88.121' (ED25519) to the list of known hosts.
long mode automatically triggered
/home/michtest/test-europe-new/full/geo_cells.bin : 23.68%   (  1.56 GiB =>    378 MiB, /tmp/geo_cells.bin.patch)
396408051


Good — geo_cells.bin patches at 23.7% when it actually works (the previous 0-byte was a failed run). Let me get the real total patch size including this fix.

Now let me look at this from the user's perspective and design something better. The core problem:

| File | Size | zstd Patch | Ratio |
|------|------|-----------|-------|
| geo_cells.bin | 1.56 GiB | 378 MiB | 23.7% |
| addr_points.bin | 1.28 GiB | 883 MiB | 67.3% |
| admin_vertices.bin | 1.28 GiB | 3.2 MiB | 0.2% |
| street_nodes.bin | 1.35 GiB | 6.5 MiB | 0.4% |
| **Total** | **~7 GiB** | **~1.88 GiB** | **~26%** |

The addr_points.bin alone accounts for 47% of the total patch. Let me understand why by looking at how it's built.


Agent

How addr_points IDs are assigned

Prompt:
In /root/traccar-geocoder/builder/src/build_index.cpp, find how address points get their IDs assigned. Specifically:
1. How are addr_points collected during Pass 2 (node scanning)?
2. How are they stored/sorted before writing to addr_points.bin?
3. What determines the order of records in addr_points.bin?
4. Similarly, how are street_ways ordered — what determines a way's position/ID in street_ways.bin?

I want to understand whether the ordering is deterministic and what it depends on (e.g., PBF file order, hash map iteration order, S2 cell order, etc.)


In /root/traccar-geocoder/builder/src/build_index.cpp, find how address points get their IDs assigned. Specifically:
1. How are addr_points collected during Pass 2 (node scanning)?
2. How are they stored/sorted before writing to addr_points.bin?
3. What determines the order of records in addr_points.bin?
4. Similarly, how are street_ways ordered — what determines a way's position/ID in street_ways.bin?

I want to understand whether the ordering is deterministic and what it depends on (e.g., PBF file order, hash map iteration order, S2 cell order, etc.)

Prompt:
In /root/traccar-geocoder/builder/src/build_index.cpp, find how address points get their IDs assigned. Specifically:
1. How are addr_points collected during Pass 2 (node scanning)?
2. How are they stored/sorted before writing to addr_points.bin?
3. What determines the order of records in addr_points.bin?
4. Similarly, how are street_ways ordered — what determines a way's position/ID in street_ways.bin?

I want to understand whether the ordering is deterministic and what it depends on (e.g., PBF file order, hash map iteration order, S2 cell order, etc.)


Error: File content (22670 tokens) exceeds maximum allowed tokens (10000). Use offset and limit parameters to read specific portions of the file, or search for specific content instead of reading the whole file.


336-            };
337-            std::cerr << "  Admin assembly needs " << admin_way_count << " way geometries." << std::endl;
338-
339:            // --- Pass 2: Node processing (streaming — no PbfNode objects) ---
340:            std::cerr << "  Pass 2: processing nodes with " << num_threads << " threads..." << std::endl;
341-            {
342-                struct NodeThreadLocal {
343-                    std::vector<std::pair<double,double>> addr_coords;
--
400-                std::cerr << "  Node processing complete: " << total_addrs
401-                          << " address points collected." << std::endl;
402-            }
403:            log_phase("Pass 2: node processing", _pt, _cpu);
404-            pbf.release_pages(); // free PBF mmap pages, will re-fault for way pass
405-
406:            // --- Pass 2b: Way processing (fully parallel) ---
407-            std::cerr << "  Processing ways with " << num_threads << " threads..." << std::endl;
408-            {
409-                // Thread-local data for parallel way processing
--
693-
694-                // --- Parallel admin boundary assembly ---
695-                {
696:                    log_phase("Pass 2b: way processing", _pt, _cpu);
697-                    pbf.unmap(); // release 86 GiB PBF mmap before admin/S2 phases
698-                    std::cerr << "  Assembling admin polygons in parallel ("
699-                              << data.collected_relations.size() << " relations, "
--
913-
914-        std::cerr << "Done reading:" << std::endl;
915-        std::cerr << "  " << data.ways.size() << " street ways" << std::endl;
916:        std::cerr << "  " << data.addr_points.size() << " address points" << std::endl;
917-        std::cerr << "  " << data.interp_ways.size() << " interpolation ways" << std::endl;
918-        std::cerr << "  " << data.admin_polygons.size() << " admin polygon rings" << std::endl;
919-
--
1491-            "admin_polygons.bin", "admin_vertices.bin"
1492-        };
1493-        std::vector<std::string> street_files = {
1494:            "street_ways.bin", "street_nodes.bin", "street_entries.bin"
1495-        };
1496-        std::vector<std::string> address_files = {
1497:            "addr_points.bin", "addr_entries.bin",
1498-            "interp_ways.bin", "interp_nodes.bin", "interp_entries.bin"
1499-        };
1500-        std::vector<std::string> geo_files = {"geo_cells.bin"};


1489-        };
1490-        std::vector<std::string> admin_quality_files = {
1491-            "admin_polygons.bin", "admin_vertices.bin"
1492-        };
1493-        std::vector<std::string> street_files = {
1494:            "street_ways.bin", "street_nodes.bin", "street_entries.bin"
1495-        };
1496-        std::vector<std::string> address_files = {
1497:            "addr_points.bin", "addr_entries.bin",
1498-            "interp_ways.bin", "interp_nodes.bin", "interp_entries.bin"
1499-        };
1500-        std::vector<std::string> geo_files = {"geo_cells.bin"};
1501-
1502-        // Build list of regions


339-            // --- Pass 2: Node processing (streaming — no PbfNode objects) ---
340-            std::cerr << "  Pass 2: processing nodes with " << num_threads << " threads..." << std::endl;
341-            {
342-                struct NodeThreadLocal {
343:                    std::vector<std::pair<double,double>> addr_coords;
344-                    std::vector<std::pair<std::string,std::string>> addr_strings;
345-                    uint64_t count = 0;
346-                };
347-                // Use thread_local for streaming callback (no thread index available)
--
375-                                    street = tag_vals[i] < st.size() ? st[tag_vals[i]].c_str() : nullptr;
376-                            }
377-                        }
378-                        if (housenumber && street) {
379:                            tl_node_data->addr_coords.push_back({lat, lng});
380-                            tl_node_data->addr_strings.push_back({housenumber, street});
381-                            tl_node_data->count++;
382-                        }
383-                    }
--
388-
389-                // Merge address points
390-                uint64_t total_addrs = 0;
391-                for (auto& local : ntld) {
392:                    for (size_t j = 0; j < local.addr_coords.size(); j++) {
393-                        uint64_t dummy = 0;
394:                        add_addr_point(data, local.addr_coords[j].first, local.addr_coords[j].second,
395-                                       local.addr_strings[j].first.c_str(),
396-                                       local.addr_strings[j].second.c_str(), dummy);
397-                    }
398-                    total_addrs += local.count;
--
414-                    std::vector<InterpWay> interp_ways;
415-                    std::vector<NodeCoord> interp_nodes;
416-                    std::vector<DeferredInterp> deferred_interps;
417-                    std::vector<AddrPoint> building_addrs;
418:                    std::vector<std::pair<double, double>> building_addr_coords; // lat,lng for S2 cell
419-                    std::vector<std::string> way_strings;      // way name strings
420-                    std::vector<std::pair<std::string,std::string>> addr_strings; // building addr {hn, street}
421-                    std::vector<std::string> interp_strings;   // interp street name strings
422-                    uint64_t way_count = 0;
--
536-                            for (const auto& loc : resolved_locs) {
537-                                if (loc.valid()) { sum_lat += loc.lat(); sum_lng += loc.lon(); valid++; }
538-                            }
539-                            if (valid > 0) {
540:                                local.building_addr_coords.push_back({sum_lat/valid, sum_lng/valid});
541-                                local.building_addrs.push_back({static_cast<float>(sum_lat/valid), static_cast<float>(sum_lng/valid), 0, 0});
542-                                local.addr_strings.push_back({housenumber, street});
543-                                local.building_addr_count++;
544-                            }
--
912-        }
913-
914-        std::cerr << "Done reading:" << std::endl;
915-        std::cerr << "  " << data.ways.size() << " street ways" << std::endl;
916:        std::cerr << "  " << data.addr_points.size() << " address points" << std::endl;
917-        std::cerr << "  " << data.interp_ways.size() << " interpolation ways" << std::endl;
918-        std::cerr << "  " << data.admin_polygons.size() << " admin polygon rings" << std::endl;
919-
920-        // Drain admin polygon thread pool (may still be processing)
--
1493-        std::vector<std::string> street_files = {
1494-            "street_ways.bin", "street_nodes.bin", "street_entries.bin"
1495-        };
1496-        std::vector<std::string> address_files = {
1497:            "addr_points.bin", "addr_entries.bin",
1498-            "interp_ways.bin", "interp_nodes.bin", "interp_entries.bin"
1499-        };
1500-        std::vector<std::string> geo_files = {"geo_cells.bin"};
1501-


389-                // Merge address points
390-                uint64_t total_addrs = 0;
391-                for (auto& local : ntld) {
392-                    for (size_t j = 0; j < local.addr_coords.size(); j++) {
393-                        uint64_t dummy = 0;
394:                        add_addr_point(data, local.addr_coords[j].first, local.addr_coords[j].second,
395-                                       local.addr_strings[j].first.c_str(),
396-                                       local.addr_strings[j].second.c_str(), dummy);
397-                    }
398-                    total_addrs += local.count;
399-                }
--
628-                    }
629-
630-                    // Merge building addresses
631-                    for (size_t i = 0; i < local.building_addrs.size(); i++) {
632-                        uint64_t dummy = 0;
633:                        add_addr_point(data, local.building_addrs[i].lat, local.building_addrs[i].lng,
634-                                       local.addr_strings[i].first.c_str(),
635-                                       local.addr_strings[i].second.c_str(), dummy);
636-                    }
637-
638-                    // Merge interpolation ways
--
911-            }
912-        }
913-
914-        std::cerr << "Done reading:" << std::endl;
915-        std::cerr << "  " << data.ways.size() << " street ways" << std::endl;
916:        std::cerr << "  " << data.addr_points.size() << " address points" << std::endl;
917-        std::cerr << "  " << data.interp_ways.size() << " interpolation ways" << std::endl;
918-        std::cerr << "  " << data.admin_polygons.size() << " admin polygon rings" << std::endl;
919-
920-        // Drain admin polygon thread pool (may still be processing)
921-        std::cerr << "Waiting for admin polygon S2 covering to complete..." << std::endl;


384-                });
385-
386-                // Reset thread_local for next use
387-                tl_node_data = nullptr;
388-
389-                // Merge address points
390-                uint64_t total_addrs = 0;
391-                for (auto& local : ntld) {
392-                    for (size_t j = 0; j < local.addr_coords.size(); j++) {
393-                        uint64_t dummy = 0;
394:                        add_addr_point(data, local.addr_coords[j].first, local.addr_coords[j].second,
395-                                       local.addr_strings[j].first.c_str(),
396-                                       local.addr_strings[j].second.c_str(), dummy);
397-                    }
398-                    total_addrs += local.count;
399-                }
400-                std::cerr << "  Node processing complete: " << total_addrs
401-                          << " address points collected." << std::endl;
402-            }
403-            log_phase("Pass 2: node processing", _pt, _cpu);
404-            pbf.release_pages(); // free PBF mmap pages, will re-fault for way pass
--
623-                    // Remap deferred ways
624-                    for (auto dw : local.deferred_ways) {
625-                        dw.way_id += way_base;
626-                        dw.node_offset += node_base;
627-                        data.deferred_ways.push_back(dw);
628-                    }
629-
630-                    // Merge building addresses
631-                    for (size_t i = 0; i < local.building_addrs.size(); i++) {
632-                        uint64_t dummy = 0;
633:                        add_addr_point(data, local.building_addrs[i].lat, local.building_addrs[i].lng,
634-                                       local.addr_strings[i].first.c_str(),
635-                                       local.addr_strings[i].second.c_str(), dummy);
636-                    }
637-
638-                    // Merge interpolation ways
639-                    for (size_t i = 0; i < local.interp_ways.size(); i++) {
640-                        auto iw = local.interp_ways[i];
641-                        iw.node_offset += interp_node_base;
642-                        iw.street_id = data.string_pool.intern(local.interp_strings[i]);
643-                        data.interp_ways.push_back(iw);


Bash

grep -n "^.*add_addr_point" /root/traccar-geocoder/builder/src/build_index.cpp | head -20


394:                        add_addr_point(data, local.addr_coords[j].first, local.addr_coords[j].second,
633:                        add_addr_point(data, local.building_addrs[i].lat, local.building_addrs[i].lng,


find /root/traccar-geocoder/builder/src -name "*.h" -o -name "*.hpp" | head -20


/root/traccar-geocoder/builder/src/ring_assembly.h
/root/traccar-geocoder/builder/src/string_pool.h
/root/traccar-geocoder/builder/src/interpolation.h
/root/traccar-geocoder/builder/src/cache.h
/root/traccar-geocoder/builder/src/s2_helpers.h
/root/traccar-geocoder/builder/src/continent_boundaries.h
/root/traccar-geocoder/builder/src/cell_index.h
/root/traccar-geocoder/builder/src/pbf_reader.h
/root/traccar-geocoder/builder/src/relation_collector.h
/root/traccar-geocoder/builder/src/continent_filter.h
/root/traccar-geocoder/builder/src/parsed_data.h
/root/traccar-geocoder/builder/src/types.h
/root/traccar-geocoder/builder/src/geometry.h


     1→#pragma once
     2→
     3→#include <algorithm>
     4→#include <chrono>
     5→#include <iostream>
     6→#include <string>
     7→#include <unordered_map>
     8→#include <vector>
     9→
    10→#include <cstdio>
    11→#include <cstring>
    12→#include <dirent.h>
    13→#include <iomanip>
    14→#include <thread>
    15→#include <unistd.h>
    16→#include <sys/stat.h>
    17→
    18→#include "types.h"
    19→#include "string_pool.h"
    20→
    21→// --- Directory creation ---
    22→
    23→inline void ensure_dir(const std::string& path) {
    24→    // Recursive mkdir — create all parent directories
    25→    for (size_t i = 1; i < path.size(); i++) {
    26→        if (path[i] == '/') {
    27→            mkdir(path.substr(0, i).c_str(), 0755);
    28→        }
    29→    }
    30→    mkdir(path.c_str(), 0755);
    31→}
    32→
    33→// --- CPU tick reading (all threads via getrusage) ---
    34→
    35→#include <sys/resource.h>
    36→
    37→struct CpuTicks {
    38→    long long process_us = 0;  // user+system microseconds (all threads)
    39→
    40→    static CpuTicks now() {
    41→        CpuTicks ct;
    42→        struct rusage ru;
    43→        if (getrusage(RUSAGE_SELF, &ru) == 0) {
    44→            ct.process_us = (long long)ru.ru_utime.tv_sec * 1000000 + ru.ru_utime.tv_usec
    45→                          + (long long)ru.ru_stime.tv_sec * 1000000 + ru.ru_stime.tv_usec;
    46→        }
    47→        return ct;
    48→    }
    49→};
    50→
    51→// --- Phase timer with CPU utilization ---
    52→
    53→inline long get_rss_mb() {
    54→    long rss_pages = 0;
    55→    FILE* f = fopen("/proc/self/statm", "r");
    56→    if (f) {
    57→        long size;
    58→        if (fscanf(f, "%ld %ld", &size, &rss_pages) != 2) rss_pages = 0;
    59→        fclose(f);
    60→    }
    61→    return rss_pages * sysconf(_SC_PAGESIZE) / (1024 * 1024);
    62→}
    63→
    64→inline void log_phase(const char* name, std::chrono::steady_clock::time_point& t,
    65→                       CpuTicks& prev_cpu) {
    66→    auto now = std::chrono::steady_clock::now();
    67→    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - t).count();
    68→    auto cpu_now = CpuTicks::now();
    69→    double cpu_sec = (cpu_now.process_us - prev_cpu.process_us) / 1e6;
    70→    double wall_sec = ms / 1000.0;
    71→    double cores_used = (wall_sec > 0.1) ? cpu_sec / wall_sec : 0;
    72→    unsigned ncpu = std::thread::hardware_concurrency();
    73→    int pct = (wall_sec > 0.1 && ncpu > 0) ? (int)(cores_used * 100.0 / ncpu) : 0;
    74→    long rss = get_rss_mb();
    75→    std::cerr << "  [" << ms/1000 << "." << (ms%1000)/100 << "s"
    76→              << " " << std::fixed << std::setprecision(1) << cores_used << "/" << ncpu << "cores"
    77→              << " " << pct << "%"
    78→              << " " << rss << "MiB] " << name << std::endl;
    79→    t = now;
    80→    prev_cpu = cpu_now;
    81→}
    82→
    83→// Backward compat overload (no CPU tracking)
    84→inline void log_phase(const char* name, std::chrono::steady_clock::time_point& t) {
    85→    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
    86→        std::chrono::steady_clock::now() - t).count();
    87→    std::cerr << "  [" << ms/1000 << "." << (ms%1000)/100 << "s] " << name << std::endl;
    88→    t = std::chrono::steady_clock::now();
    89→}
    90→
    91→// --- Parsed data container ---
    92→
    93→struct ParsedData {
    94→    StringPool string_pool;
    95→    std::vector<WayHeader> ways;
    96→    std::vector<NodeCoord> street_nodes;
    97→    std::unordered_map<uint64_t, std::vector<uint32_t>> cell_to_ways;
    98→    std::vector<AddrPoint> addr_points;
    99→    std::unordered_map<uint64_t, std::vector<uint32_t>> cell_to_addrs;
   100→    std::vector<InterpWay> interp_ways;
   101→    std::vector<NodeCoord> interp_nodes;
   102→    std::unordered_map<uint64_t, std::vector<uint32_t>> cell_to_interps;
   103→    std::vector<AdminPolygon> admin_polygons;
   104→    std::vector<NodeCoord> admin_vertices;
   105→    std::unordered_map<uint64_t, std::vector<uint32_t>> cell_to_admin;
   106→
   107→    // Deferred work for parallel S2 computation (ways + interps)
   108→    std::vector<DeferredWay> deferred_ways;
   109→    std::vector<DeferredInterp> deferred_interps;
   110→
   111→    // Sorted (cell_id, item_id) pairs — kept for direct entry writing
   112→    std::vector<CellItemPair> sorted_way_cells;
   113→    std::vector<CellItemPair> sorted_addr_cells;
   114→    std::vector<CellItemPair> sorted_interp_cells;
   115→
   116→    // Collected data for parallel admin assembly
   117→    std::vector<CollectedRelation> collected_relations;
   118→    struct WayGeometry {
   119→        std::vector<std::pair<double,double>> coords;
   120→        int64_t first_node_id;
   121→        int64_t last_node_id;
   122→    };
   123→    std::unordered_map<int64_t, WayGeometry> way_geometries;
   124→};
   125→
   126→// --- Deduplicate IDs per cell ---
   127→
   128→template<typename Map>
   129→inline void deduplicate(Map& cell_map) {
   130→    for (auto& [cell_id, ids] : cell_map) {
   131→        std::sort(ids.begin(), ids.end());
   132→        ids.erase(std::unique(ids.begin(), ids.end()), ids.end());
   133→    }
   134→}
   135→


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


     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→        return 1;
    62→    }
    63→
    64→    std::string output_dir = argv[1];
    65→    std::vector<std::string> input_files;
    66→    std::string save_cache_path;
    67→    std::string load_cache_path;
    68→    bool multi_output = false;
    69→    bool generate_continents = false;
    70→    IndexMode mode = IndexMode::Full;
    71→    SimplifyMode simplify_mode = SimplifyMode::MaxVertices;
    72→    double simplify_epsilon_override = 0; // 0 = use per-level defaults
    73→    bool multi_quality = false;
    74→    std::vector<double> quality_scales;
    75→
    76→    for (int i = 2; i < argc; i++) {
    77→        std::string arg = argv[i];
    78→        if (arg == "--street-level" && i + 1 < argc) {
    79→            kStreetCellLevel = std::atoi(argv[++i]);
    80→        } else if (arg == "--admin-level" && i + 1 < argc) {
    81→            kAdminCellLevel = std::atoi(argv[++i]);
    82→        } else if (arg == "--admin-only") {
    83→            mode = IndexMode::AdminOnly;
    84→        } else if (arg == "--no-addresses") {
    85→            mode = IndexMode::NoAddresses;
    86→        } else if (arg == "--max-admin-level" && i + 1 < argc) {
    87→            kMaxAdminLevel = std::atoi(argv[++i]);
    88→        } else if (arg == "--save-cache" && i + 1 < argc) {
    89→            save_cache_path = argv[++i];
    90→        } else if (arg == "--load-cache" && i + 1 < argc) {
    91→            load_cache_path = argv[++i];
    92→        } else if (arg == "--multi-output") {
    93→            multi_output = true;
    94→        } else if (arg == "--continents") {
    95→            generate_continents = true;
    96→        } else if (arg == "--simplify-epsilon") {
    97→            simplify_mode = SimplifyMode::ErrorBounded;
    98→            // Optional: next arg might be a number (fixed epsilon in meters)
    99→            if (i + 1 < argc) {
   100→                char* end;
   101→                double val = std::strtod(argv[i+1], &end);
   102→                if (*end == '\0' && val > 0) {
   103→                    simplify_epsilon_override = val;
   104→                    i++;
   105→                }
   106→            }
   107→        } else if (arg == "--epsilon-scale" && i + 1 < argc) {
   108→            kEpsilonScale = std::strtod(argv[++i], nullptr);
   109→        } else if (arg == "--multi-quality") {
   110→            multi_quality = true;
   111→            // Optional: next arg might be comma-separated scales
   112→            if (i + 1 < argc && argv[i+1][0] != '-') {
   113→                std::string vals = argv[++i];
   114→                size_t pos = 0;
   115→                while (pos < vals.size()) {
   116→                    size_t next = vals.find(',', pos);
   117→                    if (next == std::string::npos) next = vals.size();
   118→                    double v = std::strtod(vals.substr(pos, next - pos).c_str(), nullptr);
   119→                    quality_scales.push_back(v);
   120→                    pos = next + 1;
   121→                }
   122→            }
   123→            if (quality_scales.empty()) {
   124→                quality_scales = {0, 0.2, 0.5, 1.0, 1.5, 2.0, 2.5};
   125→            }
   126→            // Build with uncapped polygons — quality variants simplify at write time.
   127→            // We need full vertex detail to produce all quality levels accurately.
   128→            simplify_mode = SimplifyMode::ErrorBounded;
   129→            simplify_epsilon_override = 0.1; // effectively uncapped
   130→        } else if (arg == "--epsilon-levels" && i + 1 < argc) {
   131→            // Parse comma-separated values: L2,L3,L4,L5,L6,L7,L8
   132→            std::string vals = argv[++i];
   133→            int level = 2;
   134→            size_t pos = 0;
   135→            while (pos < vals.size() && level <= 8) {
   136→                size_t next = vals.find(',', pos);
   137→                if (next == std::string::npos) next = vals.size();
   138→                double v = std::strtod(vals.substr(pos, next - pos).c_str(), nullptr);
   139→                if (v > 0) kAdminEpsilonMeters[level] = v;
   140→                level++;
   141→                pos = next + 1;
   142→            }
   143→            // Also set levels 9-11 to same as level 8
   144→            for (int l = level; l <= 11; l++) kAdminEpsilonMeters[l] = kAdminEpsilonMeters[8];
   145→        } else if (arg == "--mode" && i + 1 < argc) {
   146→            std::string mode_str = argv[++i];
   147→            if (mode_str == "full") {
   148→                mode = IndexMode::Full;
   149→            } else if (mode_str == "no-addresses") {
   150→                mode = IndexMode::NoAddresses;
   151→            } else if (mode_str == "admin-only") {
   152→                mode = IndexMode::AdminOnly;
   153→            } else {
   154→                std::cerr << "Error: unknown mode '" << mode_str << "'" << std::endl;
   155→                return 1;
   156→            }
   157→        } else {
   158→            input_files.push_back(arg);
   159→        }
   160→    }
   161→
   162→    // Set global simplification mode
   163→    kSimplifyMode = simplify_mode;
   164→    kSimplifyEpsilonOverride = simplify_epsilon_override;
   165→    if (simplify_mode == SimplifyMode::ErrorBounded) {
   166→        if (simplify_epsilon_override > 0) {
   167→            std::cerr << "Simplification: error-bounded, fixed " << simplify_epsilon_override << "m epsilon" << std::endl;
   168→        } else {
   169→            std::cerr << "Simplification: error-bounded, scale=" << kEpsilonScale
   170→                      << ", epsilons: L2=" << admin_epsilon_meters(2)
   171→                      << "m L4=" << admin_epsilon_meters(4)
   172→                      << "m L6=" << admin_epsilon_meters(6)
   173→                      << "m L8=" << admin_epsilon_meters(8) << "m" << std::endl;
   174→        }
   175→    }
   176→
   177→    ParsedData data;
   178→    auto _pt = std::chrono::steady_clock::now();
   179→    auto _cpu = CpuTicks::now();
   180→
   181→    if (!load_cache_path.empty()) {
   182→        // Load from cache
   183→        if (!deserialize_cache(data, load_cache_path)) {
   184→            std::cerr << "Error: failed to load cache" << std::endl;
   185→            return 1;
   186→        }
   187→    } else {
   188→        // Parse PBF files (always collect everything)
   189→        if (input_files.empty()) {
   190→            std::cerr << "Error: no input files specified and no --load-cache" << std::endl;
   191→            return 1;
   192→        }
   193→
   194→        // Create thread pool for concurrent admin polygon S2 covering
   195→        unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency() - 1);
   196→        std::cerr << "Using " << num_threads << " worker threads." << std::endl;
   197→        AdminCoverPool admin_pool(num_threads);
   198→        // BuildHandler no longer used — parallel processing handles everything
   199→
   200→        // Dense array node location index — lockless parallel writes


grep -B 5 -A 15 "inline.*add_addr_point\|auto.*add_addr_point\s*=" /root/traccar-geocoder/builder/src/build_index.cpp | head -60


(Bash completed with no output)


No matches found


   150→                mode = IndexMode::NoAddresses;
   151→            } else if (mode_str == "admin-only") {
   152→                mode = IndexMode::AdminOnly;
   153→            } else {
   154→                std::cerr << "Error: unknown mode '" << mode_str << "'" << std::endl;
   155→                return 1;
   156→            }
   157→        } else {
   158→            input_files.push_back(arg);
   159→        }
   160→    }
   161→
   162→    // Set global simplification mode
   163→    kSimplifyMode = simplify_mode;
   164→    kSimplifyEpsilonOverride = simplify_epsilon_override;
   165→    if (simplify_mode == SimplifyMode::ErrorBounded) {
   166→        if (simplify_epsilon_override > 0) {
   167→            std::cerr << "Simplification: error-bounded, fixed " << simplify_epsilon_override << "m epsilon" << std::endl;
   168→        } else {
   169→            std::cerr << "Simplification: error-bounded, scale=" << kEpsilonScale
   170→                      << ", epsilons: L2=" << admin_epsilon_meters(2)
   171→                      << "m L4=" << admin_epsilon_meters(4)
   172→                      << "m L6=" << admin_epsilon_meters(6)
   173→                      << "m L8=" << admin_epsilon_meters(8) << "m" << std::endl;
   174→        }
   175→    }
   176→
   177→    ParsedData data;
   178→    auto _pt = std::chrono::steady_clock::now();
   179→    auto _cpu = CpuTicks::now();
   180→
   181→    if (!load_cache_path.empty()) {
   182→        // Load from cache
   183→        if (!deserialize_cache(data, load_cache_path)) {
   184→            std::cerr << "Error: failed to load cache" << std::endl;
   185→            return 1;
   186→        }
   187→    } else {
   188→        // Parse PBF files (always collect everything)
   189→        if (input_files.empty()) {
   190→            std::cerr << "Error: no input files specified and no --load-cache" << std::endl;
   191→            return 1;
   192→        }
   193→
   194→        // Create thread pool for concurrent admin polygon S2 covering
   195→        unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency() - 1);
   196→        std::cerr << "Using " << num_threads << " worker threads." << std::endl;
   197→        AdminCoverPool admin_pool(num_threads);
   198→        // BuildHandler no longer used — parallel processing handles everything
   199→
   200→        // Dense array node location index — lockless parallel writes
   201→        // Planet OSM node IDs max ~12.5 billion. 8 bytes per entry = 100GB virtual.
   202→        // MAP_NORESERVE means OS only allocates pages on write (~80GB for 10B nodes).
   203→        static const size_t MAX_NODE_ID = MAX_NODE_ID_DEFAULT;
   204→        struct PackedLocation {
   205→            int32_t lat_e7;  // latitude * 10^7
   206→            int32_t lon_e7;  // longitude * 10^7
   207→            bool valid() const { return lat_e7 != 0 || lon_e7 != 0; }
   208→            double lat() const { return lat_e7 / 10000000.0; }
   209→            double lon() const { return lon_e7 / 10000000.0; }
   210→        };
   211→        struct DenseIndex {
   212→            PackedLocation* data;
   213→            size_t capacity;
   214→
   215→            DenseIndex() : capacity(MAX_NODE_ID) {
   216→                size_t byte_size = capacity * sizeof(PackedLocation);
   217→                void* ptr = mmap(nullptr, byte_size,
   218→                    PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE,
   219→                    -1, 0);
   220→                if (ptr == MAP_FAILED) {
   221→                    std::cerr << "Error: failed to mmap dense node index ("
   222→                              << (byte_size / (1024*1024*1024)) << "GB)" << std::endl;
   223→                    std::exit(1);
   224→                }
   225→                madvise(ptr, byte_size, MADV_HUGEPAGE);
   226→                data = static_cast<PackedLocation*>(ptr);
   227→                std::cerr << "Allocated dense node index: " << (byte_size / (1024*1024*1024))
   228→                          << "GB virtual address space" << std::endl;
   229→            }
   230→
   231→            ~DenseIndex() {
   232→                munmap(data, capacity * sizeof(PackedLocation));
   233→            }
   234→
   235→            // Lockless — each node ID maps to a unique array slot
   236→            void set(uint64_t id, double lat, double lng) {
   237→                if (id >= capacity) return;
   238→                data[id] = {static_cast<int32_t>(lat * 10000000.0 + (lat >= 0 ? 0.5 : -0.5)),
   239→                            static_cast<int32_t>(lng * 10000000.0 + (lng >= 0 ? 0.5 : -0.5))};
   240→            }
   241→
   242→            PackedLocation get(uint64_t id) const {
   243→                if (id >= capacity) return {0, 0};
   244→                return data[id];
   245→            }
   246→        } index;
   247→
   248→        for (const auto& input_file : input_files) {
   249→            std::cerr << "Processing " << input_file << "..." << std::endl;
   250→
   251→            // --- Pass 1: collect relation members for parallel admin assembly ---
   252→            std::cerr << "  Pass 1: scanning relations..." << std::endl;
   253→            PbfFile pbf(input_file, num_threads);
   254→
   255→            {
   256→                std::mutex rel_mutex;
   257→                pbf.read_blocks([&](PbfBlock& block, unsigned) {
   258→                    for (auto& rel : block.relations) {
   259→                        const char* boundary = rel.tag("boundary");
   260→                        if (!boundary) continue;
   261→
   262→                        bool is_admin = (std::strcmp(boundary, "administrative") == 0);
   263→                        bool is_postal = (std::strcmp(boundary, "postal_code") == 0);
   264→                        if (!is_admin && !is_postal) continue;
   265→
   266→                        uint8_t admin_level = 0;
   267→                        if (is_admin) {
   268→                            const char* level_str = rel.tag("admin_level");
   269→                            if (!level_str) continue;
   270→                            admin_level = static_cast<uint8_t>(std::atoi(level_str));
   271→                            if (admin_level < 2 || admin_level > 10) continue;
   272→                        } else {
   273→                            admin_level = 11;
   274→                        }
   275→                        if (kMaxAdminLevel > 0 && admin_level > kMaxAdminLevel) continue;
   276→
   277→                        const char* name = rel.tag("name");
   278→                        if (!name && is_admin) continue;
   279→
   280→                        std::string name_str;
   281→                        if (is_postal) {
   282→                            const char* postal_code = rel.tag("postal_code");
   283→                            if (!postal_code) postal_code = name;
   284→                            if (!postal_code) continue;
   285→                            name_str = postal_code;
   286→                        } else {
   287→                            name_str = name;
   288→                        }
   289→
   290→                        std::string country_code;
   291→                        if (admin_level == 2) {
   292→                            const char* cc = rel.tag("ISO3166-1:alpha2");
   293→                            if (cc) country_code = cc;
   294→                        }
   295→
   296→                        CollectedRelation cr;
   297→                        cr.id = rel.id;
   298→                        cr.admin_level = admin_level;
   299→                        cr.name = std::move(name_str);
   300→                        cr.country_code = std::move(country_code);
   301→                        cr.is_postal = is_postal;
   302→
   303→                        for (size_t mi = 0; mi < rel.members.size(); mi++) {
   304→                            if (rel.members[mi].type == 'w') {
   305→                                cr.members.emplace_back(rel.members[mi].ref, rel.member_role(mi));
   306→                            }
   307→                        }
   308→
   309→                        if (!cr.members.empty()) {
   310→                            std::lock_guard<std::mutex> lock(rel_mutex);
   311→                            data.collected_relations.push_back(std::move(cr));
   312→                        }
   313→                    }
   314→                }, "r");
   315→            }
   316→            std::cerr << "  Collected " << data.collected_relations.size()
   317→                      << " admin/postal relations for parallel assembly." << std::endl;
   318→            log_phase("Pass 1: relation scanning", _pt, _cpu);
   319→
   320→            // Build admin_way_ids as a bitset for O(1) lookup
   321→            // Max way ID ~1.3B → ~162 MiB bitset (much faster than unordered_set)
   322→            static constexpr size_t MAX_WAY_ID = 2000000000ULL; // 2B, ~250 MiB
   323→            std::vector<uint8_t> admin_way_bits(MAX_WAY_ID / 8 + 1, 0);
   324→            size_t admin_way_count = 0;
   325→            for (const auto& rel : data.collected_relations) {
   326→                for (const auto& [way_id, role] : rel.members) {
   327→                    if (way_id > 0 && static_cast<size_t>(way_id) < MAX_WAY_ID) {
   328→                        admin_way_bits[way_id / 8] |= (1 << (way_id % 8));
   329→                        admin_way_count++;
   330→                    }
   331→                }
   332→            }
   333→            auto is_admin_way = [&](int64_t id) -> bool {
   334→                if (id <= 0 || static_cast<size_t>(id) >= MAX_WAY_ID) return false;
   335→                return admin_way_bits[id / 8] & (1 << (id % 8));
   336→            };
   337→            std::cerr << "  Admin assembly needs " << admin_way_count << " way geometries." << std::endl;
   338→
   339→            // --- Pass 2: Node processing (streaming — no PbfNode objects) ---
   340→            std::cerr << "  Pass 2: processing nodes with " << num_threads << " threads..." << std::endl;
   341→            {
   342→                struct NodeThreadLocal {
   343→                    std::vector<std::pair<double,double>> addr_coords;
   344→                    std::vector<std::pair<std::string,std::string>> addr_strings;
   345→                    uint64_t count = 0;
   346→                };
   347→                // Use thread_local for streaming callback (no thread index available)
   348→                static thread_local NodeThreadLocal* tl_node_data = nullptr;
   349→                std::vector<NodeThreadLocal> ntld(num_threads);
   350→                std::atomic<unsigned> next_tl{0};
   351→
   352→                pbf.read_nodes_streaming([&](int64_t id, double lat, double lng,
   353→                    const uint32_t* tag_keys, const uint32_t* tag_vals, size_t ntags,
   354→                    const std::vector<std::string>& st) {
   355→
   356→                    // Lazy init thread-local pointer
   357→                    if (!tl_node_data) {
   358→                        unsigned idx = next_tl.fetch_add(1);
   359→                        tl_node_data = &ntld[idx % ntld.size()];
   360→                    }
   361→
   362→                    if (id > 0) {
   363→                        index.set(static_cast<uint64_t>(id), lat, lng);
   364→                    }
   365→
   366→                    // Check for address tags (fast — no string copies for tagless nodes)
   367→                    if (ntags > 0) {
   368→                        const char* housenumber = nullptr;
   369→                        const char* street = nullptr;
   370→                        for (size_t i = 0; i < ntags; i++) {
   371→                            if (tag_keys[i] < st.size()) {
   372→                                if (st[tag_keys[i]] == "addr:housenumber")
   373→                                    housenumber = tag_vals[i] < st.size() ? st[tag_vals[i]].c_str() : nullptr;
   374→                                else if (st[tag_keys[i]] == "addr:street")
   375→                                    street = tag_vals[i] < st.size() ? st[tag_vals[i]].c_str() : nullptr;
   376→                            }
   377→                        }
   378→                        if (housenumber && street) {
   379→                            tl_node_data->addr_coords.push_back({lat, lng});
   380→                            tl_node_data->addr_strings.push_back({housenumber, street});
   381→                            tl_node_data->count++;
   382→                        }
   383→                    }
   384→                });
   385→
   386→                // Reset thread_local for next use
   387→                tl_node_data = nullptr;
   388→
   389→                // Merge address points
   390→                uint64_t total_addrs = 0;
   391→                for (auto& local : ntld) {
   392→                    for (size_t j = 0; j < local.addr_coords.size(); j++) {
   393→                        uint64_t dummy = 0;
   394→                        add_addr_point(data, local.addr_coords[j].first, local.addr_coords[j].second,
   395→                                       local.addr_strings[j].first.c_str(),
   396→                                       local.addr_strings[j].second.c_str(), dummy);
   397→                    }
   398→                    total_addrs += local.count;
   399→                }


grep -r "add_addr_point" /root/traccar-geocoder/builder/src/*.h 2>/dev/null


/root/traccar-geocoder/builder/src/s2_helpers.h:void add_addr_point(ParsedData& data, double lat, double lng,