Now let me also address the cross-version comparison issue. Let me check the explore agent's findings about the way count difference.


Bash

cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/a83cc828100c311e9.output 2>/dev/null | tail -40

Check explore agent progress


The agent is still running. Let me compile and test the ring stitcher fix while waiting.


cat /root/traccar-geocoder/builder/src/build_index.cpp | ssh node3 "cat > /tmp/traccar-geocoder/builder/src/build_index.cpp" && ssh node3 "cd /tmp/traccar-geocoder/build && make -j\$(nproc) 2>&1 | tail -5" 2>&1

Upload and compile ring stitcher fix on node3


[ 50%] Building CXX object CMakeFiles/build-index.dir/src/build_index.cpp.o
[100%] Linking CXX executable build-index
[100%] Built target build-index


ssh node3 "rm -rf /tmp/de-parallel-bt && mkdir -p /tmp/de-parallel-bt && /tmp/traccar-geocoder/build/build-index /tmp/de-parallel-bt ~/pbf/germany.osm.pbf --parallel-admin 2>&1 | tail -10" 2>&1

Run parallel build with backtracking ring stitcher


Command running in background with ID: bj0h5bxgn. Output is being written to: /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/bj0h5bxgn.output


While that builds, let me also commit this change and investigate the cross-version comparison.


git add builder/src/build_index.cpp && git commit -m "$(cat <<'EOF'
fix: use backtracking ring stitcher to handle branch points

The greedy ring stitcher picked the first matching candidate at branch
points (where multiple ways share an endpoint node). If it picked the
wrong branch, the ring couldn't close and the polygon was lost.

Replace with a backtracking algorithm that tries all candidates at each
branch point, finding a valid ring closure if one exists. Uses adjacency
map for O(1) endpoint lookups.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EOF
)"

Commit ring stitcher fix


[main a5817ac] fix: use backtracking ring stitcher to handle branch points
 1 file changed, 79 insertions(+), 40 deletions(-)


Now let me investigate the cross-version address comparison issue. The same address count (19,951,156) but only 20% match suggests a systematic difference. Let me check how addresses are stored differently.


ssh node3 "cat > /tmp/debug_addr.cpp << 'EOFCPP'
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iostream>
#include <vector>

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

static std::vector<char> read_file(const std::string& path) {
    std::ifstream f(path, std::ios::binary | std::ios::ate);
    if (!f) return {};
    auto sz = f.tellg(); f.seekg(0);
    std::vector<char> d(sz); f.read(d.data(), sz); return d;
}

int main(int argc, char* argv[]) {
    for (int a = 1; a < argc; a++) {
        std::string dir = argv[a];
        auto strings = read_file(dir + \"/strings.bin\");
        auto data = read_file(dir + \"/addr_points.bin\");
        size_t count = data.size() / sizeof(AddrPoint);
        auto* pts = (AddrPoint*)data.data();

        printf(\"%s: %zu addrs, strings.bin=%zu bytes\\n\", dir.c_str(), count, strings.size());
        for (size_t i = 0; i < 5 && i < count; i++) {
            const char* hn = (pts[i].housenumber_id < strings.size()) ? strings.data() + pts[i].housenumber_id : \"?\";
            const char* st = (pts[i].street_id < strings.size()) ? strings.data() + pts[i].street_id : \"?\";
            uint32_t hlat, hlng;
            memcpy(&hlat, &pts[i].lat, 4);
            memcpy(&hlng, &pts[i].lng, 4);
            printf(\"  [%zu] lat=%.7f lng=%.7f hn='%s' st='%s' (hex: %08x %08x hn_id=%u st_id=%u)\\n\",
                   i, pts[i].lat, pts[i].lng, hn, st, hlat, hlng, pts[i].housenumber_id, pts[i].street_id);
        }
        printf(\"\\n\");
    }
    return 0;
}
EOFCPP
g++ -O2 -std=c++17 -o /tmp/debug_addr /tmp/debug_addr.cpp && /tmp/debug_addr /tmp/de-baseline /tmp/de-parallel-fixed" 2>&1

Compare address data format between baseline and new code


/tmp/de-baseline: 19951156 addrs, strings.bin=7556971 bytes
  [0] lat=50.9155922 lng=6.9412465 hn='107' st='Höninger Weg' (hex: 424ba991 40de1eb1 hn_id=0 st_id=4)
  [1] lat=48.1039543 lng=11.4883080 hn='7' st='Lorettoplatz' (hex: 42406a73 4137d01c hn_id=18 st_id=20)
  [2] lat=55.0181656 lng=8.4217558 hn='1' st='Landwehrdeich' (hex: 425c129a 4106bf83 hn_id=33 st_id=35)
  [3] lat=48.3551788 lng=10.8930874 hn='30' st='Morellstraße' (hex: 42416bb4 412e4a16 hn_id=49 st_id=52)
  [4] lat=51.0054131 lng=13.8066912 hn='22' st='Langer Weg' (hex: 424c058b 415ce835 hn_id=66 st_id=69)

/tmp/de-parallel-fixed: 19951156 addrs, strings.bin=149049831 bytes
  [0] lat=48.1401749 lng=11.5777607 hn='4' st='Max-Joseph-Platz' (hex: 42408f8a 41393e82 hn_id=0 st_id=2)
  [1] lat=49.5142975 lng=8.5469503 hn='1' st='Magdeburger Straße' (hex: 42460ea4 4108c04f hn_id=19 st_id=21)
  [2] lat=52.9818535 lng=8.8492718 hn='14' st='Am Marktplatz' (hex: 4253ed6b 410d969e hn_id=41 st_id=44)
  [3] lat=51.2277794 lng=12.7250566 hn='3' st='Colditzer Weg' (hex: 424ce93f 414b99d5 hn_id=58 st_id=60)
  [4] lat=50.1958275 lng=8.6568718 hn='7' st='Züricher Straße' (hex: 4248c887 410a828c hn_id=74 st_id=76)


Let me check the backtracking ring stitcher build progress.


ssh node3 "ps aux | grep 'build-index /tmp/de-parallel-bt' | grep -v grep | awk '{print \$6/1024\"MB\", \$11, \$12}'" 2>&1

Check backtracking build progress


3.44922MB bash -c
108677MB /tmp/traccar-geocoder/build/build-index /tmp/de-parallel-bt


Still processing (108GB RSS). While that runs, let me update the comparison tool with a cross-version "semantic" mode that matches entities by name rather than exact coordinates.


   250→                way_nodes.push_back({nodes[ni].lat, nodes[ni].lng});
   251→            }
   252→        }
   253→        fps.push_back(way_fingerprint(name, way_nodes));
   254→    }
   255→    return fps;
   256→}
   257→
   258→static std::vector<std::string> fingerprint_addrs(const std::string& dir) {
   259→    auto strings = read_file(dir + "/strings.bin");
   260→    auto points = read_structs<AddrPoint>(dir + "/addr_points.bin");
   261→
   262→    std::vector<std::string> fps;
   263→    fps.reserve(points.size());
   264→    for (const auto& p : points) {
   265→        std::string hn = get_string(strings, p.housenumber_id);
   266→        std::string st = get_string(strings, p.street_id);
   267→        fps.push_back(addr_fingerprint(p.lat, p.lng, hn, st));
   268→    }
   269→    return fps;
   270→}
   271→
   272→static std::vector<std::string> fingerprint_interps(const std::string& dir) {
   273→    auto strings = read_file(dir + "/strings.bin");
   274→    auto interps = read_structs<InterpWay>(dir + "/interp_ways.bin");
   275→    auto nodes = read_structs<NodeCoord>(dir + "/interp_nodes.bin");
   276→
   277→    std::vector<std::string> fps;
   278→    fps.reserve(interps.size());
   279→    for (const auto& iw : interps) {
   280→        std::string street = get_string(strings, iw.street_id);
   281→        std::vector<std::pair<float,float>> way_nodes;
   282→        for (uint8_t i = 0; i < iw.node_count; i++) {
   283→            uint32_t ni = iw.node_offset + i;
   284→            if (ni < nodes.size()) {
   285→                way_nodes.push_back({nodes[ni].lat, nodes[ni].lng});
   286→            }
   287→        }
   288→        fps.push_back(interp_fingerprint(street, iw.start_number, iw.end_number,
   289→                                          iw.interpolation, way_nodes));
   290→    }
   291→    return fps;
   292→}
   293→
   294→int main(int argc, char* argv[]) {
   295→    if (argc != 3) {
   296→        std::cerr << "Usage: compare_indexes <dir-A> <dir-B>" << std::endl;
   297→        return 2;
   298→    }
   299→

                way_nodes.push_back({nodes[ni].lat, nodes[ni].lng});
            }
        }
        fps.push_back(way_fingerprint(name, way_nodes));
    }
    return fps;
}

static std::vector<std::string> fingerprint_addrs(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto points = read_structs<AddrPoint>(dir + "/addr_points.bin");

    std::vector<std::string> fps;
    fps.reserve(points.size());
    for (const auto& p : points) {
        std::string hn = get_string(strings, p.housenumber_id);
        std::string st = get_string(strings, p.street_id);
        fps.push_back(addr_fingerprint(p.lat, p.lng, hn, st));
    }
    return fps;
}

static std::vector<std::string> fingerprint_interps(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto interps = read_structs<InterpWay>(dir + "/interp_ways.bin");
    auto nodes = read_structs<NodeCoord>(dir + "/interp_nodes.bin");

    std::vector<std::string> fps;
    fps.reserve(interps.size());
    for (const auto& iw : interps) {
        std::string street = get_string(strings, iw.street_id);
        std::vector<std::pair<float,float>> way_nodes;
        for (uint8_t i = 0; i < iw.node_count; i++) {
            uint32_t ni = iw.node_offset + i;
            if (ni < nodes.size()) {
                way_nodes.push_back({nodes[ni].lat, nodes[ni].lng});
            }
        }
        fps.push_back(interp_fingerprint(street, iw.start_number, iw.end_number,
                                          iw.interpolation, way_nodes));
    }
    return fps;
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        std::cerr << "Usage: compare_indexes <dir-A> <dir-B>" << std::endl;
        return 2;
    }


   294→int main(int argc, char* argv[]) {
   295→    if (argc != 3) {
   296→        std::cerr << "Usage: compare_indexes <dir-A> <dir-B>" << std::endl;
   297→        return 2;
   298→    }
   299→
   300→    std::string dir_a = argv[1];
   301→    std::string dir_b = argv[2];
   302→
   303→    std::cout << "Comparing:" << std::endl;
   304→    std::cout << "  A: " << dir_a << std::endl;
   305→    std::cout << "  B: " << dir_b << std::endl;
   306→    std::cout << std::endl;
   307→
   308→    bool all_pass = true;
   309→
   310→    // Compare admin polygons
   311→    {
   312→        std::cout << "Loading admin polygons..." << std::flush;
   313→        auto fps_a = fingerprint_admin(dir_a);
   314→        auto fps_b = fingerprint_admin(dir_b);
   315→        std::cout << " done." << std::endl;
   316→        if (!compare_multisets("admin_polygons", fps_a, fps_b))
   317→            all_pass = false;
   318→    }
   319→
   320→    // Compare street ways
   321→    {
   322→        std::cout << "Loading street ways..." << std::flush;
   323→        auto fps_a = fingerprint_ways(dir_a);
   324→        auto fps_b = fingerprint_ways(dir_b);
   325→        std::cout << " done." << std::endl;
   326→        if (!compare_multisets("street_ways", fps_a, fps_b))
   327→            all_pass = false;
   328→    }
   329→
   330→    // Compare address points
   331→    {
   332→        std::cout << "Loading address points..." << std::flush;
   333→        auto fps_a = fingerprint_addrs(dir_a);
   334→        auto fps_b = fingerprint_addrs(dir_b);
   335→        std::cout << " done." << std::endl;
   336→        if (!compare_multisets("address_points", fps_a, fps_b))
   337→            all_pass = false;
   338→    }
   339→
   340→    // Compare interpolation ways
   341→    {
   342→        std::cout << "Loading interpolation ways..." << std::flush;
   343→        auto fps_a = fingerprint_interps(dir_a);
   344→        auto fps_b = fingerprint_interps(dir_b);
   345→        std::cout << " done." << std::endl;
   346→        if (!compare_multisets("interp_ways", fps_a, fps_b))
   347→            all_pass = false;
   348→    }
   349→
   350→    // Compare cell index sizes (should be identical regardless of ordering)
   351→    {
   352→        auto ac = read_file(dir_a + "/admin_cells.bin");
   353→        auto bc = read_file(dir_b + "/admin_cells.bin");
   354→        if (ac == bc) {
   355→            std::cout << "PASS: admin_cells.bin byte-identical (" << ac.size() << " bytes)" << std::endl;
   356→        } else if (ac.size() == bc.size()) {
   357→            std::cout << "INFO: admin_cells.bin same size (" << ac.size() << " bytes) but different content" << std::endl;
   358→        } else {

int main(int argc, char* argv[]) {
    if (argc != 3) {
        std::cerr << "Usage: compare_indexes <dir-A> <dir-B>" << std::endl;
        return 2;
    }

    std::string dir_a = argv[1];
    std::string dir_b = argv[2];

    std::cout << "Comparing:" << std::endl;
    std::cout << "  A: " << dir_a << std::endl;
    std::cout << "  B: " << dir_b << std::endl;
    std::cout << std::endl;

    bool all_pass = true;

    // Compare admin polygons
    {
        std::cout << "Loading admin polygons..." << std::flush;
        auto fps_a = fingerprint_admin(dir_a);
        auto fps_b = fingerprint_admin(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("admin_polygons", fps_a, fps_b))
            all_pass = false;
    }

    // Compare street ways
    {
        std::cout << "Loading street ways..." << std::flush;
        auto fps_a = fingerprint_ways(dir_a);
        auto fps_b = fingerprint_ways(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("street_ways", fps_a, fps_b))
            all_pass = false;
    }

    // Compare address points
    {
        std::cout << "Loading address points..." << std::flush;
        auto fps_a = fingerprint_addrs(dir_a);
        auto fps_b = fingerprint_addrs(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("address_points", fps_a, fps_b))
            all_pass = false;
    }

    // Compare interpolation ways
    {
        std::cout << "Loading interpolation ways..." << std::flush;
        auto fps_a = fingerprint_interps(dir_a);
        auto fps_b = fingerprint_interps(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("interp_ways", fps_a, fps_b))
            all_pass = false;
    }

    // Compare cell index sizes (should be identical regardless of ordering)
    {
        auto ac = read_file(dir_a + "/admin_cells.bin");
        auto bc = read_file(dir_b + "/admin_cells.bin");
        if (ac == bc) {
            std::cout << "PASS: admin_cells.bin byte-identical (" << ac.size() << " bytes)" << std::endl;
        } else if (ac.size() == bc.size()) {
            std::cout << "INFO: admin_cells.bin same size (" << ac.size() << " bytes) but different content" << std::endl;
        } else {


Let me add a `--semantic` mode to the comparison tool that matches by name/attributes only, tolerant of coordinate differences.


Edit

int main(int argc, char* argv[]) {
    if (argc != 3) {
        std::cerr << "Usage: compare_indexes <dir-A> <dir-B>" << std::endl;
        return 2;
    }

    std::string dir_a = argv[1];
    std::string dir_b = argv[2];

// --- Semantic (cross-version) fingerprints ---
// Match by name/attributes only, ignoring coordinate precision differences

// Way: name + node_count only
static std::string way_semantic_fp(const std::string& name, size_t node_count) {
    return name + "|" + std::to_string(node_count);
}

// Address: housenumber + street (ignore coords — centroid may differ across versions)
static std::string addr_semantic_fp(const std::string& housenumber, const std::string& street) {
    return housenumber + "|" + street;
}

// Admin: name + level
static std::string admin_semantic_fp(const std::string& name, uint8_t admin_level) {
    return name + "|" + std::to_string(admin_level);
}

static std::vector<std::string> semantic_fp_ways(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto ways = read_structs<WayHeader>(dir + "/street_ways.bin");

    std::vector<std::string> fps;
    fps.reserve(ways.size());
    for (const auto& w : ways) {
        std::string name = get_string(strings, w.name_id);
        fps.push_back(way_semantic_fp(name, w.node_count));
    }
    return fps;
}

static std::vector<std::string> semantic_fp_addrs(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto points = read_structs<AddrPoint>(dir + "/addr_points.bin");

    std::vector<std::string> fps;
    fps.reserve(points.size());
    for (const auto& p : points) {
        std::string hn = get_string(strings, p.housenumber_id);
        std::string st = get_string(strings, p.street_id);
        fps.push_back(addr_semantic_fp(hn, st));
    }
    return fps;
}

static std::vector<std::string> semantic_fp_admin(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto polygons = read_structs<AdminPolygon>(dir + "/admin_polygons.bin");

    std::vector<std::string> fps;
    fps.reserve(polygons.size());
    for (const auto& p : polygons) {
        std::string name = get_string(strings, p.name_id);
        fps.push_back(admin_semantic_fp(name, p.admin_level));
    }
    return fps;
}

int main(int argc, char* argv[]) {
    if (argc < 3) {
        std::cerr << "Usage: compare_indexes <dir-A> <dir-B> [--semantic]" << std::endl;
        return 2;
    }

    std::string dir_a = argv[1];
    std::string dir_b = argv[2];
    bool semantic = (argc > 3 && std::string(argv[3]) == "--semantic");


The file /root/traccar-geocoder/builder/test/compare_indexes.cpp has been updated successfully.

// --- Semantic (cross-version) fingerprints ---
// Match by name/attributes only, ignoring coordinate precision differences

// Way: name + node_count only
static std::string way_semantic_fp(const std::string& name, size_t node_count) {
    return name + "|" + std::to_string(node_count);
}

// Address: housenumber + street (ignore coords — centroid may differ across versions)
static std::string addr_semantic_fp(const std::string& housenumber, const std::string& street) {
    return housenumber + "|" + street;
}

// Admin: name + level
static std::string admin_semantic_fp(const std::string& name, uint8_t admin_level) {
    return name + "|" + std::to_string(admin_level);
}

static std::vector<std::string> semantic_fp_ways(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto ways = read_structs<WayHeader>(dir + "/street_ways.bin");

    std::vector<std::string> fps;
    fps.reserve(ways.size());
    for (const auto& w : ways) {
        std::string name = get_string(strings, w.name_id);
        fps.push_back(way_semantic_fp(name, w.node_count));
    }
    return fps;
}

static std::vector<std::string> semantic_fp_addrs(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto points = read_structs<AddrPoint>(dir + "/addr_points.bin");

    std::vector<std::string> fps;
    fps.reserve(points.size());
    for (const auto& p : points) {
        std::string hn = get_string(strings, p.housenumber_id);
        std::string st = get_string(strings, p.street_id);
        fps.push_back(addr_semantic_fp(hn, st));
    }
    return fps;
}

static std::vector<std::string> semantic_fp_admin(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto polygons = read_structs<AdminPolygon>(dir + "/admin_polygons.bin");

    std::vector<std::string> fps;
    fps.reserve(polygons.size());
    for (const auto& p : polygons) {
        std::string name = get_string(strings, p.name_id);
        fps.push_back(admin_semantic_fp(name, p.admin_level));
    }
    return fps;
}

int main(int argc, char* argv[]) {
    if (argc < 3) {
        std::cerr << "Usage: compare_indexes <dir-A> <dir-B> [--semantic]" << std::endl;
        return 2;
    }

    std::string dir_a = argv[1];
    std::string dir_b = argv[2];
    bool semantic = (argc > 3 && std::string(argv[3]) == "--semantic");

// Semantic comparison of two geocoder index directories.
// Decodes all binary files, creates canonical fingerprints for each item,
// and compares as multisets — handles any ordering differences between
// parallel and sequential builds.
// Returns 0 if semantically identical, 1 if different.
//
// Usage: compare_indexes <dir-A> <dir-B>

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>

// --- Binary structs (must match build_index.cpp and server main.rs) ---

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;
    uint16_t vertex_count;
    uint32_t name_id;
    uint8_t admin_level;
    float area;
    uint16_t country_code;
};

struct NodeCoord {
    float lat;
    float lng;
};

// --- Helpers ---

static std::vector<char> read_file(const std::string& path) {
    std::ifstream f(path, std::ios::binary | std::ios::ate);
    if (!f) return {};
    auto size = f.tellg();
    f.seekg(0);
    std::vector<char> data(size);
    f.read(data.data(), size);
    return data;
}

static const char* get_string(const std::vector<char>& pool, uint32_t offset) {
    if (offset >= pool.size()) return "(invalid)";
    return pool.data() + offset;
}

template<typename T>
static std::vector<T> read_structs(const std::string& path) {
    auto data = read_file(path);
    size_t count = data.size() / sizeof(T);
    std::vector<T> result(count);
    memcpy(result.data(), data.data(), count * sizeof(T));
    return result;
}

// Round a float coordinate to an integer grid (1e5 ~ 1m precision)
static int to_grid(float v) {
    return (int)(v * 1e5f + (v >= 0 ? 0.5f : -0.5f));
}

// --- Canonical fingerprint builders ---
// Each function produces a string that uniquely identifies an item,
// independent of storage order.

// For a way: "name|sorted_node_coords"
// Nodes are kept in original order (path order matters for ways),
// but we also provide a sorted-set version for order-independent matching.
static std::string way_fingerprint(const std::string& name,
                                    const std::vector<std::pair<float,float>>& nodes) {
    // Use sorted coordinate set as the canonical form.
    // Two ways are "the same" if they have the same name and same set of nodes.
    std::vector<std::pair<int,int>> grid_nodes;
    grid_nodes.reserve(nodes.size());
    for (const auto& n : nodes) {
        grid_nodes.push_back({to_grid(n.first), to_grid(n.second)});
    }
    std::sort(grid_nodes.begin(), grid_nodes.end());

    std::string fp = name + "|" + std::to_string(nodes.size());
    for (const auto& g : grid_nodes) {
        fp += "|";
        fp += std::to_string(g.first);
        fp += ",";
        fp += std::to_string(g.second);
    }
    return fp;
}

// For an address: "grid_lat,grid_lng|housenumber|street"
static std::string addr_fingerprint(float lat, float lng,
                                     const std::string& housenumber,
                                     const std::string& street) {
    return std::to_string(to_grid(lat)) + "," + std::to_string(to_grid(lng)) +
           "|" + housenumber + "|" + street;
}

// For an admin polygon: "name|level|country|sorted_vertices"
static std::string admin_fingerprint(const std::string& name,
                                      uint8_t admin_level,
                                      uint16_t country_code,
                                      const std::vector<std::pair<float,float>>& vertices) {
    std::vector<std::pair<int,int>> grid_verts;
    grid_verts.reserve(vertices.size());
    for (const auto& v : vertices) {
        grid_verts.push_back({to_grid(v.first), to_grid(v.second)});
    }
    std::sort(grid_verts.begin(), grid_verts.end());

    std::string fp = name + "|" + std::to_string(admin_level) + "|" + std::to_string(country_code);
    for (const auto& g : grid_verts) {
        fp += "|";
        fp += std::to_string(g.first);
        fp += ",";
        fp += std::to_string(g.second);
    }
    return fp;
}

// For interpolation ways: "street|start|end|interp|sorted_nodes"
static std::string interp_fingerprint(const std::string& street,
                                       uint32_t start_number, uint32_t end_number,
                                       uint8_t interpolation,
                                       const std::vector<std::pair<float,float>>& nodes) {
    std::vector<std::pair<int,int>> grid_nodes;
    grid_nodes.reserve(nodes.size());
    for (const auto& n : nodes) {
        grid_nodes.push_back({to_grid(n.first), to_grid(n.second)});
    }
    std::sort(grid_nodes.begin(), grid_nodes.end());

    std::string fp = street + "|" + std::to_string(start_number) + "-" +
                     std::to_string(end_number) + "|" + std::to_string(interpolation);
    for (const auto& g : grid_nodes) {
        fp += "|";
        fp += std::to_string(g.first);
        fp += ",";
        fp += std::to_string(g.second);
    }
    return fp;
}

// --- Multiset comparison ---
// Compare two collections by their fingerprints. Reports missing/extra items.

static bool compare_multisets(const char* label,
                               const std::vector<std::string>& fps_a,
                               const std::vector<std::string>& fps_b) {
    // Build frequency maps
    std::unordered_map<std::string, int> freq_a, freq_b;
    for (const auto& fp : fps_a) freq_a[fp]++;
    for (const auto& fp : fps_b) freq_b[fp]++;

    // Find items in A but not in B (missing from B)
    size_t missing = 0;
    for (const auto& [fp, count] : freq_a) {
        int b_count = 0;
        auto it = freq_b.find(fp);
        if (it != freq_b.end()) b_count = it->second;
        if (count > b_count) missing += (count - b_count);
    }

    // Find items in B but not in A (extra in B)
    size_t extra = 0;
    for (const auto& [fp, count] : freq_b) {
        int a_count = 0;
        auto it = freq_a.find(fp);
        if (it != freq_a.end()) a_count = it->second;
        if (count > a_count) extra += (count - a_count);
    }

    size_t matched = fps_a.size() - missing;

    if (missing == 0 && extra == 0) {
        std::cout << "PASS: " << label << " (" << fps_a.size() << " items, all matched)" << std::endl;
        return true;
    } else {
        std::cerr << "FAIL: " << label << " — A=" << fps_a.size() << " B=" << fps_b.size()
                  << " matched=" << matched
                  << " only_in_A=" << missing
                  << " only_in_B=" << extra << std::endl;
        return false;
    }
}

// --- Decode + fingerprint functions ---

static std::vector<std::string> fingerprint_admin(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto polygons = read_structs<AdminPolygon>(dir + "/admin_polygons.bin");
    auto vertices = read_structs<NodeCoord>(dir + "/admin_vertices.bin");

    std::vector<std::string> fps;
    fps.reserve(polygons.size());
    for (const auto& p : polygons) {
        std::string name = get_string(strings, p.name_id);
        std::vector<std::pair<float,float>> verts;
        for (uint16_t i = 0; i < p.vertex_count; i++) {
            uint32_t vi = p.vertex_offset + i;
            if (vi < vertices.size()) {
                verts.push_back({vertices[vi].lat, vertices[vi].lng});
            }
        }
        fps.push_back(admin_fingerprint(name, p.admin_level, p.country_code, verts));
    }
    return fps;
}

static std::vector<std::string> fingerprint_ways(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto ways = read_structs<WayHeader>(dir + "/street_ways.bin");
    auto nodes = read_structs<NodeCoord>(dir + "/street_nodes.bin");

    std::vector<std::string> fps;
    fps.reserve(ways.size());
    for (const auto& w : ways) {
        std::string name = get_string(strings, w.name_id);
        std::vector<std::pair<float,float>> way_nodes;
        for (uint8_t i = 0; i < w.node_count; i++) {
            uint32_t ni = w.node_offset + i;
            if (ni < nodes.size()) {
                way_nodes.push_back({nodes[ni].lat, nodes[ni].lng});
            }
        }
        fps.push_back(way_fingerprint(name, way_nodes));
    }
    return fps;
}

static std::vector<std::string> fingerprint_addrs(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto points = read_structs<AddrPoint>(dir + "/addr_points.bin");

    std::vector<std::string> fps;
    fps.reserve(points.size());
    for (const auto& p : points) {
        std::string hn = get_string(strings, p.housenumber_id);
        std::string st = get_string(strings, p.street_id);
        fps.push_back(addr_fingerprint(p.lat, p.lng, hn, st));
    }
    return fps;
}

static std::vector<std::string> fingerprint_interps(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto interps = read_structs<InterpWay>(dir + "/interp_ways.bin");
    auto nodes = read_structs<NodeCoord>(dir + "/interp_nodes.bin");

    std::vector<std::string> fps;
    fps.reserve(interps.size());
    for (const auto& iw : interps) {
        std::string street = get_string(strings, iw.street_id);
        std::vector<std::pair<float,float>> way_nodes;
        for (uint8_t i = 0; i < iw.node_count; i++) {
            uint32_t ni = iw.node_offset + i;
            if (ni < nodes.size()) {
                way_nodes.push_back({nodes[ni].lat, nodes[ni].lng});
            }
        }
        fps.push_back(interp_fingerprint(street, iw.start_number, iw.end_number,
                                          iw.interpolation, way_nodes));
    }
    return fps;
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        std::cerr << "Usage: compare_indexes <dir-A> <dir-B>" << std::endl;
        return 2;
    }

    std::string dir_a = argv[1];
    std::string dir_b = argv[2];

    std::cout << "Comparing:" << std::endl;
    std::cout << "  A: " << dir_a << std::endl;
    std::cout << "  B: " << dir_b << std::endl;
    std::cout << std::endl;

    bool all_pass = true;

    // Compare admin polygons
    {
        std::cout << "Loading admin polygons..." << std::flush;
        auto fps_a = fingerprint_admin(dir_a);
        auto fps_b = fingerprint_admin(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("admin_polygons", fps_a, fps_b))
            all_pass = false;
    }

    // Compare street ways
    {
        std::cout << "Loading street ways..." << std::flush;
        auto fps_a = fingerprint_ways(dir_a);
        auto fps_b = fingerprint_ways(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("street_ways", fps_a, fps_b))
            all_pass = false;
    }

    // Compare address points
    {
        std::cout << "Loading address points..." << std::flush;
        auto fps_a = fingerprint_addrs(dir_a);
        auto fps_b = fingerprint_addrs(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("address_points", fps_a, fps_b))
            all_pass = false;
    }

    // Compare interpolation ways
    {
        std::cout << "Loading interpolation ways..." << std::flush;
        auto fps_a = fingerprint_interps(dir_a);
        auto fps_b = fingerprint_interps(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("interp_ways", fps_a, fps_b))
            all_pass = false;
    }

    // Compare cell index sizes (should be identical regardless of ordering)
    {
        auto ac = read_file(dir_a + "/admin_cells.bin");
        auto bc = read_file(dir_b + "/admin_cells.bin");
        if (ac == bc) {
            std::cout << "PASS: admin_cells.bin byte-identical (" << ac.size() << " bytes)" << std::endl;
        } else if (ac.size() == bc.size()) {
            std::cout << "INFO: admin_cells.bin same size (" << ac.size() << " bytes) but different content" << std::endl;
        } else {
            std::cerr << "FAIL: admin_cells.bin size differs (A=" << ac.size()
                      << " B=" << bc.size() << ")" << std::endl;
            all_pass = false;
        }

        auto gc_a = read_file(dir_a + "/geo_cells.bin");
        auto gc_b = read_file(dir_b + "/geo_cells.bin");
        if (gc_a.size() == gc_b.size()) {
            std::cout << "PASS: geo_cells.bin same size (" << gc_a.size() << " bytes)" << std::endl;
        } else {
            std::cerr << "FAIL: geo_cells.bin size differs (A=" << gc_a.size()
                      << " B=" << gc_b.size() << ")" << std::endl;
            all_pass = false;
        }
    }

    std::cout << std::endl;
    if (all_pass) {
        std::cout << "=== ALL COMPARISONS PASSED ===" << std::endl;
        return 0;
    } else {
        std::cout << "=== SOME COMPARISONS FAILED ===" << std::endl;
        return 1;
    }
}

+// --- Semantic (cross-version) fingerprints ---

+// Match by name/attributes only, ignoring coordinate precision differences

+static std::string way_semantic_fp(const std::string& name, size_t node_count) {

+    return name + "|" + std::to_string(node_count);

+// Address: housenumber + street (ignore coords — centroid may differ across versions)

+static std::string addr_semantic_fp(const std::string& housenumber, const std::string& street) {

+static std::string admin_semantic_fp(const std::string& name, uint8_t admin_level) {

+    return name + "|" + std::to_string(admin_level);

+static std::vector<std::string> semantic_fp_ways(const std::string& dir) {

+    auto strings = read_file(dir + "/strings.bin");

+    auto ways = read_structs<WayHeader>(dir + "/street_ways.bin");

+        std::string name = get_string(strings, w.name_id);

+        fps.push_back(way_semantic_fp(name, w.node_count));

+static std::vector<std::string> semantic_fp_addrs(const std::string& dir) {

+    auto points = read_structs<AddrPoint>(dir + "/addr_points.bin");

+        std::string hn = get_string(strings, p.housenumber_id);

+        std::string st = get_string(strings, p.street_id);

+        fps.push_back(addr_semantic_fp(hn, st));

+static std::vector<std::string> semantic_fp_admin(const std::string& dir) {

+    auto polygons = read_structs<AdminPolygon>(dir + "/admin_polygons.bin");

+        std::string name = get_string(strings, p.name_id);

+        fps.push_back(admin_semantic_fp(name, p.admin_level));

-        std::cerr << "Usage: compare_indexes <dir-A> <dir-B>" << std::endl;

+        std::cerr << "Usage: compare_indexes <dir-A> <dir-B> [--semantic]" << std::endl;

+    bool semantic = (argc > 3 && std::string(argv[3]) == "--semantic");


Now update the main comparison logic to use semantic mode when requested.


    std::cout << "Comparing:" << std::endl;
    std::cout << "  A: " << dir_a << std::endl;
    std::cout << "  B: " << dir_b << std::endl;
    std::cout << std::endl;

    bool all_pass = true;

    // Compare admin polygons
    {
        std::cout << "Loading admin polygons..." << std::flush;
        auto fps_a = fingerprint_admin(dir_a);
        auto fps_b = fingerprint_admin(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("admin_polygons", fps_a, fps_b))
            all_pass = false;
    }

    // Compare street ways
    {
        std::cout << "Loading street ways..." << std::flush;
        auto fps_a = fingerprint_ways(dir_a);
        auto fps_b = fingerprint_ways(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("street_ways", fps_a, fps_b))
            all_pass = false;
    }

    // Compare address points
    {
        std::cout << "Loading address points..." << std::flush;
        auto fps_a = fingerprint_addrs(dir_a);
        auto fps_b = fingerprint_addrs(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("address_points", fps_a, fps_b))
            all_pass = false;
    }

    // Compare interpolation ways
    {
        std::cout << "Loading interpolation ways..." << std::flush;
        auto fps_a = fingerprint_interps(dir_a);
        auto fps_b = fingerprint_interps(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("interp_ways", fps_a, fps_b))
            all_pass = false;
    }

    std::cout << "Comparing" << (semantic ? " (semantic/cross-version)" : " (exact)") << ":" << std::endl;
    std::cout << "  A: " << dir_a << std::endl;
    std::cout << "  B: " << dir_b << std::endl;
    std::cout << std::endl;

    bool all_pass = true;

    // Compare admin polygons
    {
        std::cout << "Loading admin polygons..." << std::flush;
        auto fps_a = semantic ? semantic_fp_admin(dir_a) : fingerprint_admin(dir_a);
        auto fps_b = semantic ? semantic_fp_admin(dir_b) : fingerprint_admin(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("admin_polygons", fps_a, fps_b))
            all_pass = false;
    }

    // Compare street ways
    {
        std::cout << "Loading street ways..." << std::flush;
        auto fps_a = semantic ? semantic_fp_ways(dir_a) : fingerprint_ways(dir_a);
        auto fps_b = semantic ? semantic_fp_ways(dir_b) : fingerprint_ways(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("street_ways", fps_a, fps_b))
            all_pass = false;
    }

    // Compare address points
    {
        std::cout << "Loading address points..." << std::flush;
        auto fps_a = semantic ? semantic_fp_addrs(dir_a) : fingerprint_addrs(dir_a);
        auto fps_b = semantic ? semantic_fp_addrs(dir_b) : fingerprint_addrs(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("address_points", fps_a, fps_b))
            all_pass = false;
    }

    // Compare interpolation ways
    {
        std::cout << "Loading interpolation ways..." << std::flush;
        auto fps_a = fingerprint_interps(dir_a);
        auto fps_b = fingerprint_interps(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("interp_ways", fps_a, fps_b))
            all_pass = false;
    }


    std::cout << "Comparing" << (semantic ? " (semantic/cross-version)" : " (exact)") << ":" << std::endl;
    std::cout << "  A: " << dir_a << std::endl;
    std::cout << "  B: " << dir_b << std::endl;
    std::cout << std::endl;

    bool all_pass = true;

    // Compare admin polygons
    {
        std::cout << "Loading admin polygons..." << std::flush;
        auto fps_a = semantic ? semantic_fp_admin(dir_a) : fingerprint_admin(dir_a);
        auto fps_b = semantic ? semantic_fp_admin(dir_b) : fingerprint_admin(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("admin_polygons", fps_a, fps_b))
            all_pass = false;
    }

    // Compare street ways
    {
        std::cout << "Loading street ways..." << std::flush;
        auto fps_a = semantic ? semantic_fp_ways(dir_a) : fingerprint_ways(dir_a);
        auto fps_b = semantic ? semantic_fp_ways(dir_b) : fingerprint_ways(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("street_ways", fps_a, fps_b))
            all_pass = false;
    }

    // Compare address points
    {
        std::cout << "Loading address points..." << std::flush;
        auto fps_a = semantic ? semantic_fp_addrs(dir_a) : fingerprint_addrs(dir_a);
        auto fps_b = semantic ? semantic_fp_addrs(dir_b) : fingerprint_addrs(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("address_points", fps_a, fps_b))
            all_pass = false;
    }

    // Compare interpolation ways
    {
        std::cout << "Loading interpolation ways..." << std::flush;
        auto fps_a = fingerprint_interps(dir_a);
        auto fps_b = fingerprint_interps(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("interp_ways", fps_a, fps_b))
            all_pass = false;
    }

// Semantic comparison of two geocoder index directories.
// Decodes all binary files, creates canonical fingerprints for each item,
// and compares as multisets — handles any ordering differences between
// parallel and sequential builds.
// Returns 0 if semantically identical, 1 if different.
//
// Usage: compare_indexes <dir-A> <dir-B>

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>

// --- Binary structs (must match build_index.cpp and server main.rs) ---

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;
    uint16_t vertex_count;
    uint32_t name_id;
    uint8_t admin_level;
    float area;
    uint16_t country_code;
};

struct NodeCoord {
    float lat;
    float lng;
};

// --- Helpers ---

static std::vector<char> read_file(const std::string& path) {
    std::ifstream f(path, std::ios::binary | std::ios::ate);
    if (!f) return {};
    auto size = f.tellg();
    f.seekg(0);
    std::vector<char> data(size);
    f.read(data.data(), size);
    return data;
}

static const char* get_string(const std::vector<char>& pool, uint32_t offset) {
    if (offset >= pool.size()) return "(invalid)";
    return pool.data() + offset;
}

template<typename T>
static std::vector<T> read_structs(const std::string& path) {
    auto data = read_file(path);
    size_t count = data.size() / sizeof(T);
    std::vector<T> result(count);
    memcpy(result.data(), data.data(), count * sizeof(T));
    return result;
}

// Round a float coordinate to an integer grid (1e5 ~ 1m precision)
static int to_grid(float v) {
    return (int)(v * 1e5f + (v >= 0 ? 0.5f : -0.5f));
}

// --- Canonical fingerprint builders ---
// Each function produces a string that uniquely identifies an item,
// independent of storage order.

// For a way: "name|sorted_node_coords"
// Nodes are kept in original order (path order matters for ways),
// but we also provide a sorted-set version for order-independent matching.
static std::string way_fingerprint(const std::string& name,
                                    const std::vector<std::pair<float,float>>& nodes) {
    // Use sorted coordinate set as the canonical form.
    // Two ways are "the same" if they have the same name and same set of nodes.
    std::vector<std::pair<int,int>> grid_nodes;
    grid_nodes.reserve(nodes.size());
    for (const auto& n : nodes) {
        grid_nodes.push_back({to_grid(n.first), to_grid(n.second)});
    }
    std::sort(grid_nodes.begin(), grid_nodes.end());

    std::string fp = name + "|" + std::to_string(nodes.size());
    for (const auto& g : grid_nodes) {
        fp += "|";
        fp += std::to_string(g.first);
        fp += ",";
        fp += std::to_string(g.second);
    }
    return fp;
}

// For an address: "grid_lat,grid_lng|housenumber|street"
static std::string addr_fingerprint(float lat, float lng,
                                     const std::string& housenumber,
                                     const std::string& street) {
    return std::to_string(to_grid(lat)) + "," + std::to_string(to_grid(lng)) +
           "|" + housenumber + "|" + street;
}

// For an admin polygon: "name|level|country|sorted_vertices"
static std::string admin_fingerprint(const std::string& name,
                                      uint8_t admin_level,
                                      uint16_t country_code,
                                      const std::vector<std::pair<float,float>>& vertices) {
    std::vector<std::pair<int,int>> grid_verts;
    grid_verts.reserve(vertices.size());
    for (const auto& v : vertices) {
        grid_verts.push_back({to_grid(v.first), to_grid(v.second)});
    }
    std::sort(grid_verts.begin(), grid_verts.end());

    std::string fp = name + "|" + std::to_string(admin_level) + "|" + std::to_string(country_code);
    for (const auto& g : grid_verts) {
        fp += "|";
        fp += std::to_string(g.first);
        fp += ",";
        fp += std::to_string(g.second);
    }
    return fp;
}

// For interpolation ways: "street|start|end|interp|sorted_nodes"
static std::string interp_fingerprint(const std::string& street,
                                       uint32_t start_number, uint32_t end_number,
                                       uint8_t interpolation,
                                       const std::vector<std::pair<float,float>>& nodes) {
    std::vector<std::pair<int,int>> grid_nodes;
    grid_nodes.reserve(nodes.size());
    for (const auto& n : nodes) {
        grid_nodes.push_back({to_grid(n.first), to_grid(n.second)});
    }
    std::sort(grid_nodes.begin(), grid_nodes.end());

    std::string fp = street + "|" + std::to_string(start_number) + "-" +
                     std::to_string(end_number) + "|" + std::to_string(interpolation);
    for (const auto& g : grid_nodes) {
        fp += "|";
        fp += std::to_string(g.first);
        fp += ",";
        fp += std::to_string(g.second);
    }
    return fp;
}

// --- Multiset comparison ---
// Compare two collections by their fingerprints. Reports missing/extra items.

static bool compare_multisets(const char* label,
                               const std::vector<std::string>& fps_a,
                               const std::vector<std::string>& fps_b) {
    // Build frequency maps
    std::unordered_map<std::string, int> freq_a, freq_b;
    for (const auto& fp : fps_a) freq_a[fp]++;
    for (const auto& fp : fps_b) freq_b[fp]++;

    // Find items in A but not in B (missing from B)
    size_t missing = 0;
    for (const auto& [fp, count] : freq_a) {
        int b_count = 0;
        auto it = freq_b.find(fp);
        if (it != freq_b.end()) b_count = it->second;
        if (count > b_count) missing += (count - b_count);
    }

    // Find items in B but not in A (extra in B)
    size_t extra = 0;
    for (const auto& [fp, count] : freq_b) {
        int a_count = 0;
        auto it = freq_a.find(fp);
        if (it != freq_a.end()) a_count = it->second;
        if (count > a_count) extra += (count - a_count);
    }

    size_t matched = fps_a.size() - missing;

    if (missing == 0 && extra == 0) {
        std::cout << "PASS: " << label << " (" << fps_a.size() << " items, all matched)" << std::endl;
        return true;
    } else {
        std::cerr << "FAIL: " << label << " — A=" << fps_a.size() << " B=" << fps_b.size()
                  << " matched=" << matched
                  << " only_in_A=" << missing
                  << " only_in_B=" << extra << std::endl;
        return false;
    }
}

// --- Decode + fingerprint functions ---

static std::vector<std::string> fingerprint_admin(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto polygons = read_structs<AdminPolygon>(dir + "/admin_polygons.bin");
    auto vertices = read_structs<NodeCoord>(dir + "/admin_vertices.bin");

    std::vector<std::string> fps;
    fps.reserve(polygons.size());
    for (const auto& p : polygons) {
        std::string name = get_string(strings, p.name_id);
        std::vector<std::pair<float,float>> verts;
        for (uint16_t i = 0; i < p.vertex_count; i++) {
            uint32_t vi = p.vertex_offset + i;
            if (vi < vertices.size()) {
                verts.push_back({vertices[vi].lat, vertices[vi].lng});
            }
        }
        fps.push_back(admin_fingerprint(name, p.admin_level, p.country_code, verts));
    }
    return fps;
}

static std::vector<std::string> fingerprint_ways(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto ways = read_structs<WayHeader>(dir + "/street_ways.bin");
    auto nodes = read_structs<NodeCoord>(dir + "/street_nodes.bin");

    std::vector<std::string> fps;
    fps.reserve(ways.size());
    for (const auto& w : ways) {
        std::string name = get_string(strings, w.name_id);
        std::vector<std::pair<float,float>> way_nodes;
        for (uint8_t i = 0; i < w.node_count; i++) {
            uint32_t ni = w.node_offset + i;
            if (ni < nodes.size()) {
                way_nodes.push_back({nodes[ni].lat, nodes[ni].lng});
            }
        }
        fps.push_back(way_fingerprint(name, way_nodes));
    }
    return fps;
}

static std::vector<std::string> fingerprint_addrs(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto points = read_structs<AddrPoint>(dir + "/addr_points.bin");

    std::vector<std::string> fps;
    fps.reserve(points.size());
    for (const auto& p : points) {
        std::string hn = get_string(strings, p.housenumber_id);
        std::string st = get_string(strings, p.street_id);
        fps.push_back(addr_fingerprint(p.lat, p.lng, hn, st));
    }
    return fps;
}

static std::vector<std::string> fingerprint_interps(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto interps = read_structs<InterpWay>(dir + "/interp_ways.bin");
    auto nodes = read_structs<NodeCoord>(dir + "/interp_nodes.bin");

    std::vector<std::string> fps;
    fps.reserve(interps.size());
    for (const auto& iw : interps) {
        std::string street = get_string(strings, iw.street_id);
        std::vector<std::pair<float,float>> way_nodes;
        for (uint8_t i = 0; i < iw.node_count; i++) {
            uint32_t ni = iw.node_offset + i;
            if (ni < nodes.size()) {
                way_nodes.push_back({nodes[ni].lat, nodes[ni].lng});
            }
        }
        fps.push_back(interp_fingerprint(street, iw.start_number, iw.end_number,
                                          iw.interpolation, way_nodes));
    }
    return fps;
}

// --- Semantic (cross-version) fingerprints ---
// Match by name/attributes only, ignoring coordinate precision differences

// Way: name + node_count only
static std::string way_semantic_fp(const std::string& name, size_t node_count) {
    return name + "|" + std::to_string(node_count);
}

// Address: housenumber + street (ignore coords — centroid may differ across versions)
static std::string addr_semantic_fp(const std::string& housenumber, const std::string& street) {
    return housenumber + "|" + street;
}

// Admin: name + level
static std::string admin_semantic_fp(const std::string& name, uint8_t admin_level) {
    return name + "|" + std::to_string(admin_level);
}

static std::vector<std::string> semantic_fp_ways(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto ways = read_structs<WayHeader>(dir + "/street_ways.bin");

    std::vector<std::string> fps;
    fps.reserve(ways.size());
    for (const auto& w : ways) {
        std::string name = get_string(strings, w.name_id);
        fps.push_back(way_semantic_fp(name, w.node_count));
    }
    return fps;
}

static std::vector<std::string> semantic_fp_addrs(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto points = read_structs<AddrPoint>(dir + "/addr_points.bin");

    std::vector<std::string> fps;
    fps.reserve(points.size());
    for (const auto& p : points) {
        std::string hn = get_string(strings, p.housenumber_id);
        std::string st = get_string(strings, p.street_id);
        fps.push_back(addr_semantic_fp(hn, st));
    }
    return fps;
}

static std::vector<std::string> semantic_fp_admin(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto polygons = read_structs<AdminPolygon>(dir + "/admin_polygons.bin");

    std::vector<std::string> fps;
    fps.reserve(polygons.size());
    for (const auto& p : polygons) {
        std::string name = get_string(strings, p.name_id);
        fps.push_back(admin_semantic_fp(name, p.admin_level));
    }
    return fps;
}

int main(int argc, char* argv[]) {
    if (argc < 3) {
        std::cerr << "Usage: compare_indexes <dir-A> <dir-B> [--semantic]" << std::endl;
        return 2;
    }

    std::string dir_a = argv[1];
    std::string dir_b = argv[2];
    bool semantic = (argc > 3 && std::string(argv[3]) == "--semantic");

    std::cout << "Comparing:" << std::endl;
    std::cout << "  A: " << dir_a << std::endl;
    std::cout << "  B: " << dir_b << std::endl;
    std::cout << std::endl;

    bool all_pass = true;

    // Compare admin polygons
    {
        std::cout << "Loading admin polygons..." << std::flush;
        auto fps_a = fingerprint_admin(dir_a);
        auto fps_b = fingerprint_admin(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("admin_polygons", fps_a, fps_b))
            all_pass = false;
    }

    // Compare street ways
    {
        std::cout << "Loading street ways..." << std::flush;
        auto fps_a = fingerprint_ways(dir_a);
        auto fps_b = fingerprint_ways(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("street_ways", fps_a, fps_b))
            all_pass = false;
    }

    // Compare address points
    {
        std::cout << "Loading address points..." << std::flush;
        auto fps_a = fingerprint_addrs(dir_a);
        auto fps_b = fingerprint_addrs(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("address_points", fps_a, fps_b))
            all_pass = false;
    }

    // Compare interpolation ways
    {
        std::cout << "Loading interpolation ways..." << std::flush;
        auto fps_a = fingerprint_interps(dir_a);
        auto fps_b = fingerprint_interps(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("interp_ways", fps_a, fps_b))
            all_pass = false;
    }

    // Compare cell index sizes (should be identical regardless of ordering)
    {
        auto ac = read_file(dir_a + "/admin_cells.bin");
        auto bc = read_file(dir_b + "/admin_cells.bin");
        if (ac == bc) {
            std::cout << "PASS: admin_cells.bin byte-identical (" << ac.size() << " bytes)" << std::endl;
        } else if (ac.size() == bc.size()) {
            std::cout << "INFO: admin_cells.bin same size (" << ac.size() << " bytes) but different content" << std::endl;
        } else {
            std::cerr << "FAIL: admin_cells.bin size differs (A=" << ac.size()
                      << " B=" << bc.size() << ")" << std::endl;
            all_pass = false;
        }

        auto gc_a = read_file(dir_a + "/geo_cells.bin");
        auto gc_b = read_file(dir_b + "/geo_cells.bin");
        if (gc_a.size() == gc_b.size()) {
            std::cout << "PASS: geo_cells.bin same size (" << gc_a.size() << " bytes)" << std::endl;
        } else {
            std::cerr << "FAIL: geo_cells.bin size differs (A=" << gc_a.size()
                      << " B=" << gc_b.size() << ")" << std::endl;
            all_pass = false;
        }
    }

    std::cout << std::endl;
    if (all_pass) {
        std::cout << "=== ALL COMPARISONS PASSED ===" << std::endl;
        return 0;
    } else {
        std::cout << "=== SOME COMPARISONS FAILED ===" << std::endl;
        return 1;
    }
}

-    std::cout << "Comparing:" << std::endl;

+    std::cout << "Comparing" << (semantic ? " (semantic/cross-version)" : " (exact)") << ":" << std::endl;

-        auto fps_a = fingerprint_admin(dir_a);

-        auto fps_b = fingerprint_admin(dir_b);

+        auto fps_a = semantic ? semantic_fp_admin(dir_a) : fingerprint_admin(dir_a);

+        auto fps_b = semantic ? semantic_fp_admin(dir_b) : fingerprint_admin(dir_b);

-        auto fps_a = fingerprint_ways(dir_a);

-        auto fps_b = fingerprint_ways(dir_b);

+        auto fps_a = semantic ? semantic_fp_ways(dir_a) : fingerprint_ways(dir_a);

+        auto fps_b = semantic ? semantic_fp_ways(dir_b) : fingerprint_ways(dir_b);

-        auto fps_a = fingerprint_addrs(dir_a);

-        auto fps_b = fingerprint_addrs(dir_b);

+        auto fps_a = semantic ? semantic_fp_addrs(dir_a) : fingerprint_addrs(dir_a);

+        auto fps_b = semantic ? semantic_fp_addrs(dir_b) : fingerprint_addrs(dir_b);


Bash

cat /root/traccar-geocoder/builder/test/compare_indexes.cpp | ssh node3 "cat > /tmp/traccar-geocoder/builder/test/compare_indexes.cpp && g++ -O2 -std=c++17 -o /tmp/compare_indexes /tmp/traccar-geocoder/builder/test/compare_indexes.cpp && echo 'Compiled'" 2>&1

Upload and compile comparison tool with semantic mode


Compiled


ssh node3 "/tmp/compare_indexes /tmp/de-baseline /tmp/de-parallel-fixed --semantic 2>&1"

Run semantic comparison: baseline vs parallel-fixed


Error: Exit code 1
Comparing (semantic/cross-version):
  A: /tmp/de-baseline
  B: /tmp/de-parallel-fixed

Loading admin polygons... done.
FAIL: admin_polygons — A=43221 B=43178 matched=43170 only_in_A=51 only_in_B=8
Loading street ways... done.
FAIL: street_ways — A=3298645 B=3312673 matched=3298645 only_in_A=0 only_in_B=14028
Loading address points... done.
FAIL: address_points — A=19951156 B=19951156 matched=3960926 only_in_A=15990230 only_in_B=15990230
Loading interpolation ways... done.
PASS: interp_ways (78 items, all matched)
INFO: admin_cells.bin same size (73236 bytes) but different content
FAIL: geo_cells.bin size differs (A=186220544 B=195412000)

=== SOME COMPARISONS FAILED ===


The semantic matching shows all baseline ways exist in the new code (0 only_in_A for ways!), but address matching is still broken due to massive housenumber+street collisions. Let me try a coarser coordinate grid for cross-version address comparison.


Edit

// Address: housenumber + street (ignore coords — centroid may differ across versions)
static std::string addr_semantic_fp(const std::string& housenumber, const std::string& street) {
    return housenumber + "|" + street;
}

// Address: housenumber + street + coarse grid coords (~100m precision)
// Centroid may differ slightly across code versions, so use coarse grid
static std::string addr_semantic_fp(float lat, float lng,
                                     const std::string& housenumber, const std::string& street) {
    int glat = (int)(lat * 1e3f + (lat >= 0 ? 0.5f : -0.5f));
    int glng = (int)(lng * 1e3f + (lng >= 0 ? 0.5f : -0.5f));
    return std::to_string(glat) + "," + std::to_string(glng) + "|" + housenumber + "|" + street;
}


// Address: housenumber + street + coarse grid coords (~100m precision)
// Centroid may differ slightly across code versions, so use coarse grid
static std::string addr_semantic_fp(float lat, float lng,
                                     const std::string& housenumber, const std::string& street) {
    int glat = (int)(lat * 1e3f + (lat >= 0 ? 0.5f : -0.5f));
    int glng = (int)(lng * 1e3f + (lng >= 0 ? 0.5f : -0.5f));
    return std::to_string(glat) + "," + std::to_string(glng) + "|" + housenumber + "|" + street;
}

// Semantic comparison of two geocoder index directories.
// Decodes all binary files, creates canonical fingerprints for each item,
// and compares as multisets — handles any ordering differences between
// parallel and sequential builds.
// Returns 0 if semantically identical, 1 if different.
//
// Usage: compare_indexes <dir-A> <dir-B>

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>

// --- Binary structs (must match build_index.cpp and server main.rs) ---

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;
    uint16_t vertex_count;
    uint32_t name_id;
    uint8_t admin_level;
    float area;
    uint16_t country_code;
};

struct NodeCoord {
    float lat;
    float lng;
};

// --- Helpers ---

static std::vector<char> read_file(const std::string& path) {
    std::ifstream f(path, std::ios::binary | std::ios::ate);
    if (!f) return {};
    auto size = f.tellg();
    f.seekg(0);
    std::vector<char> data(size);
    f.read(data.data(), size);
    return data;
}

static const char* get_string(const std::vector<char>& pool, uint32_t offset) {
    if (offset >= pool.size()) return "(invalid)";
    return pool.data() + offset;
}

template<typename T>
static std::vector<T> read_structs(const std::string& path) {
    auto data = read_file(path);
    size_t count = data.size() / sizeof(T);
    std::vector<T> result(count);
    memcpy(result.data(), data.data(), count * sizeof(T));
    return result;
}

// Round a float coordinate to an integer grid (1e5 ~ 1m precision)
static int to_grid(float v) {
    return (int)(v * 1e5f + (v >= 0 ? 0.5f : -0.5f));
}

// --- Canonical fingerprint builders ---
// Each function produces a string that uniquely identifies an item,
// independent of storage order.

// For a way: "name|sorted_node_coords"
// Nodes are kept in original order (path order matters for ways),
// but we also provide a sorted-set version for order-independent matching.
static std::string way_fingerprint(const std::string& name,
                                    const std::vector<std::pair<float,float>>& nodes) {
    // Use sorted coordinate set as the canonical form.
    // Two ways are "the same" if they have the same name and same set of nodes.
    std::vector<std::pair<int,int>> grid_nodes;
    grid_nodes.reserve(nodes.size());
    for (const auto& n : nodes) {
        grid_nodes.push_back({to_grid(n.first), to_grid(n.second)});
    }
    std::sort(grid_nodes.begin(), grid_nodes.end());

    std::string fp = name + "|" + std::to_string(nodes.size());
    for (const auto& g : grid_nodes) {
        fp += "|";
        fp += std::to_string(g.first);
        fp += ",";
        fp += std::to_string(g.second);
    }
    return fp;
}

// For an address: "grid_lat,grid_lng|housenumber|street"
static std::string addr_fingerprint(float lat, float lng,
                                     const std::string& housenumber,
                                     const std::string& street) {
    return std::to_string(to_grid(lat)) + "," + std::to_string(to_grid(lng)) +
           "|" + housenumber + "|" + street;
}

// For an admin polygon: "name|level|country|sorted_vertices"
static std::string admin_fingerprint(const std::string& name,
                                      uint8_t admin_level,
                                      uint16_t country_code,
                                      const std::vector<std::pair<float,float>>& vertices) {
    std::vector<std::pair<int,int>> grid_verts;
    grid_verts.reserve(vertices.size());
    for (const auto& v : vertices) {
        grid_verts.push_back({to_grid(v.first), to_grid(v.second)});
    }
    std::sort(grid_verts.begin(), grid_verts.end());

    std::string fp = name + "|" + std::to_string(admin_level) + "|" + std::to_string(country_code);
    for (const auto& g : grid_verts) {
        fp += "|";
        fp += std::to_string(g.first);
        fp += ",";
        fp += std::to_string(g.second);
    }
    return fp;
}

// For interpolation ways: "street|start|end|interp|sorted_nodes"
static std::string interp_fingerprint(const std::string& street,
                                       uint32_t start_number, uint32_t end_number,
                                       uint8_t interpolation,
                                       const std::vector<std::pair<float,float>>& nodes) {
    std::vector<std::pair<int,int>> grid_nodes;
    grid_nodes.reserve(nodes.size());
    for (const auto& n : nodes) {
        grid_nodes.push_back({to_grid(n.first), to_grid(n.second)});
    }
    std::sort(grid_nodes.begin(), grid_nodes.end());

    std::string fp = street + "|" + std::to_string(start_number) + "-" +
                     std::to_string(end_number) + "|" + std::to_string(interpolation);
    for (const auto& g : grid_nodes) {
        fp += "|";
        fp += std::to_string(g.first);
        fp += ",";
        fp += std::to_string(g.second);
    }
    return fp;
}

// --- Multiset comparison ---
// Compare two collections by their fingerprints. Reports missing/extra items.

static bool compare_multisets(const char* label,
                               const std::vector<std::string>& fps_a,
                               const std::vector<std::string>& fps_b) {
    // Build frequency maps
    std::unordered_map<std::string, int> freq_a, freq_b;
    for (const auto& fp : fps_a) freq_a[fp]++;
    for (const auto& fp : fps_b) freq_b[fp]++;

    // Find items in A but not in B (missing from B)
    size_t missing = 0;
    for (const auto& [fp, count] : freq_a) {
        int b_count = 0;
        auto it = freq_b.find(fp);
        if (it != freq_b.end()) b_count = it->second;
        if (count > b_count) missing += (count - b_count);
    }

    // Find items in B but not in A (extra in B)
    size_t extra = 0;
    for (const auto& [fp, count] : freq_b) {
        int a_count = 0;
        auto it = freq_a.find(fp);
        if (it != freq_a.end()) a_count = it->second;
        if (count > a_count) extra += (count - a_count);
    }

    size_t matched = fps_a.size() - missing;

    if (missing == 0 && extra == 0) {
        std::cout << "PASS: " << label << " (" << fps_a.size() << " items, all matched)" << std::endl;
        return true;
    } else {
        std::cerr << "FAIL: " << label << " — A=" << fps_a.size() << " B=" << fps_b.size()
                  << " matched=" << matched
                  << " only_in_A=" << missing
                  << " only_in_B=" << extra << std::endl;
        return false;
    }
}

// --- Decode + fingerprint functions ---

static std::vector<std::string> fingerprint_admin(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto polygons = read_structs<AdminPolygon>(dir + "/admin_polygons.bin");
    auto vertices = read_structs<NodeCoord>(dir + "/admin_vertices.bin");

    std::vector<std::string> fps;
    fps.reserve(polygons.size());
    for (const auto& p : polygons) {
        std::string name = get_string(strings, p.name_id);
        std::vector<std::pair<float,float>> verts;
        for (uint16_t i = 0; i < p.vertex_count; i++) {
            uint32_t vi = p.vertex_offset + i;
            if (vi < vertices.size()) {
                verts.push_back({vertices[vi].lat, vertices[vi].lng});
            }
        }
        fps.push_back(admin_fingerprint(name, p.admin_level, p.country_code, verts));
    }
    return fps;
}

static std::vector<std::string> fingerprint_ways(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto ways = read_structs<WayHeader>(dir + "/street_ways.bin");
    auto nodes = read_structs<NodeCoord>(dir + "/street_nodes.bin");

    std::vector<std::string> fps;
    fps.reserve(ways.size());
    for (const auto& w : ways) {
        std::string name = get_string(strings, w.name_id);
        std::vector<std::pair<float,float>> way_nodes;
        for (uint8_t i = 0; i < w.node_count; i++) {
            uint32_t ni = w.node_offset + i;
            if (ni < nodes.size()) {
                way_nodes.push_back({nodes[ni].lat, nodes[ni].lng});
            }
        }
        fps.push_back(way_fingerprint(name, way_nodes));
    }
    return fps;
}

static std::vector<std::string> fingerprint_addrs(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto points = read_structs<AddrPoint>(dir + "/addr_points.bin");

    std::vector<std::string> fps;
    fps.reserve(points.size());
    for (const auto& p : points) {
        std::string hn = get_string(strings, p.housenumber_id);
        std::string st = get_string(strings, p.street_id);
        fps.push_back(addr_fingerprint(p.lat, p.lng, hn, st));
    }
    return fps;
}

static std::vector<std::string> fingerprint_interps(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto interps = read_structs<InterpWay>(dir + "/interp_ways.bin");
    auto nodes = read_structs<NodeCoord>(dir + "/interp_nodes.bin");

    std::vector<std::string> fps;
    fps.reserve(interps.size());
    for (const auto& iw : interps) {
        std::string street = get_string(strings, iw.street_id);
        std::vector<std::pair<float,float>> way_nodes;
        for (uint8_t i = 0; i < iw.node_count; i++) {
            uint32_t ni = iw.node_offset + i;
            if (ni < nodes.size()) {
                way_nodes.push_back({nodes[ni].lat, nodes[ni].lng});
            }
        }
        fps.push_back(interp_fingerprint(street, iw.start_number, iw.end_number,
                                          iw.interpolation, way_nodes));
    }
    return fps;
}

// --- Semantic (cross-version) fingerprints ---
// Match by name/attributes only, ignoring coordinate precision differences

// Way: name + node_count only
static std::string way_semantic_fp(const std::string& name, size_t node_count) {
    return name + "|" + std::to_string(node_count);
}

// Address: housenumber + street (ignore coords — centroid may differ across versions)
static std::string addr_semantic_fp(const std::string& housenumber, const std::string& street) {
    return housenumber + "|" + street;
}

// Admin: name + level
static std::string admin_semantic_fp(const std::string& name, uint8_t admin_level) {
    return name + "|" + std::to_string(admin_level);
}

static std::vector<std::string> semantic_fp_ways(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto ways = read_structs<WayHeader>(dir + "/street_ways.bin");

    std::vector<std::string> fps;
    fps.reserve(ways.size());
    for (const auto& w : ways) {
        std::string name = get_string(strings, w.name_id);
        fps.push_back(way_semantic_fp(name, w.node_count));
    }
    return fps;
}

static std::vector<std::string> semantic_fp_addrs(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto points = read_structs<AddrPoint>(dir + "/addr_points.bin");

    std::vector<std::string> fps;
    fps.reserve(points.size());
    for (const auto& p : points) {
        std::string hn = get_string(strings, p.housenumber_id);
        std::string st = get_string(strings, p.street_id);
        fps.push_back(addr_semantic_fp(hn, st));
    }
    return fps;
}

static std::vector<std::string> semantic_fp_admin(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto polygons = read_structs<AdminPolygon>(dir + "/admin_polygons.bin");

    std::vector<std::string> fps;
    fps.reserve(polygons.size());
    for (const auto& p : polygons) {
        std::string name = get_string(strings, p.name_id);
        fps.push_back(admin_semantic_fp(name, p.admin_level));
    }
    return fps;
}

int main(int argc, char* argv[]) {
    if (argc < 3) {
        std::cerr << "Usage: compare_indexes <dir-A> <dir-B> [--semantic]" << std::endl;
        return 2;
    }

    std::string dir_a = argv[1];
    std::string dir_b = argv[2];
    bool semantic = (argc > 3 && std::string(argv[3]) == "--semantic");

    std::cout << "Comparing" << (semantic ? " (semantic/cross-version)" : " (exact)") << ":" << std::endl;
    std::cout << "  A: " << dir_a << std::endl;
    std::cout << "  B: " << dir_b << std::endl;
    std::cout << std::endl;

    bool all_pass = true;

    // Compare admin polygons
    {
        std::cout << "Loading admin polygons..." << std::flush;
        auto fps_a = semantic ? semantic_fp_admin(dir_a) : fingerprint_admin(dir_a);
        auto fps_b = semantic ? semantic_fp_admin(dir_b) : fingerprint_admin(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("admin_polygons", fps_a, fps_b))
            all_pass = false;
    }

    // Compare street ways
    {
        std::cout << "Loading street ways..." << std::flush;
        auto fps_a = semantic ? semantic_fp_ways(dir_a) : fingerprint_ways(dir_a);
        auto fps_b = semantic ? semantic_fp_ways(dir_b) : fingerprint_ways(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("street_ways", fps_a, fps_b))
            all_pass = false;
    }

    // Compare address points
    {
        std::cout << "Loading address points..." << std::flush;
        auto fps_a = semantic ? semantic_fp_addrs(dir_a) : fingerprint_addrs(dir_a);
        auto fps_b = semantic ? semantic_fp_addrs(dir_b) : fingerprint_addrs(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("address_points", fps_a, fps_b))
            all_pass = false;
    }

    // Compare interpolation ways
    {
        std::cout << "Loading interpolation ways..." << std::flush;
        auto fps_a = fingerprint_interps(dir_a);
        auto fps_b = fingerprint_interps(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("interp_ways", fps_a, fps_b))
            all_pass = false;
    }

    // Compare cell index sizes (should be identical regardless of ordering)
    {
        auto ac = read_file(dir_a + "/admin_cells.bin");
        auto bc = read_file(dir_b + "/admin_cells.bin");
        if (ac == bc) {
            std::cout << "PASS: admin_cells.bin byte-identical (" << ac.size() << " bytes)" << std::endl;
        } else if (ac.size() == bc.size()) {
            std::cout << "INFO: admin_cells.bin same size (" << ac.size() << " bytes) but different content" << std::endl;
        } else {
            std::cerr << "FAIL: admin_cells.bin size differs (A=" << ac.size()
                      << " B=" << bc.size() << ")" << std::endl;
            all_pass = false;
        }

        auto gc_a = read_file(dir_a + "/geo_cells.bin");
        auto gc_b = read_file(dir_b + "/geo_cells.bin");
        if (gc_a.size() == gc_b.size()) {
            std::cout << "PASS: geo_cells.bin same size (" << gc_a.size() << " bytes)" << std::endl;
        } else {
            std::cerr << "FAIL: geo_cells.bin size differs (A=" << gc_a.size()
                      << " B=" << gc_b.size() << ")" << std::endl;
            all_pass = false;
        }
    }

    std::cout << std::endl;
    if (all_pass) {
        std::cout << "=== ALL COMPARISONS PASSED ===" << std::endl;
        return 0;
    } else {
        std::cout << "=== SOME COMPARISONS FAILED ===" << std::endl;
        return 1;
    }
}

-// Address: housenumber + street (ignore coords — centroid may differ across versions)

-static std::string addr_semantic_fp(const std::string& housenumber, const std::string& street) {

+// Address: housenumber + street + coarse grid coords (~100m precision)

+// Centroid may differ slightly across code versions, so use coarse grid

+static std::string addr_semantic_fp(float lat, float lng,

+                                     const std::string& housenumber, const std::string& street) {

+    return std::to_string(glat) + "," + std::to_string(glng) + "|" + housenumber + "|" + street;


        std::string hn = get_string(strings, p.housenumber_id);
        std::string st = get_string(strings, p.street_id);
        fps.push_back(addr_semantic_fp(hn, st));

        std::string hn = get_string(strings, p.housenumber_id);
        std::string st = get_string(strings, p.street_id);
        fps.push_back(addr_semantic_fp(p.lat, p.lng, hn, st));


        std::string hn = get_string(strings, p.housenumber_id);
        std::string st = get_string(strings, p.street_id);
        fps.push_back(addr_semantic_fp(p.lat, p.lng, hn, st));

// Semantic comparison of two geocoder index directories.
// Decodes all binary files, creates canonical fingerprints for each item,
// and compares as multisets — handles any ordering differences between
// parallel and sequential builds.
// Returns 0 if semantically identical, 1 if different.
//
// Usage: compare_indexes <dir-A> <dir-B>

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>

// --- Binary structs (must match build_index.cpp and server main.rs) ---

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;
    uint16_t vertex_count;
    uint32_t name_id;
    uint8_t admin_level;
    float area;
    uint16_t country_code;
};

struct NodeCoord {
    float lat;
    float lng;
};

// --- Helpers ---

static std::vector<char> read_file(const std::string& path) {
    std::ifstream f(path, std::ios::binary | std::ios::ate);
    if (!f) return {};
    auto size = f.tellg();
    f.seekg(0);
    std::vector<char> data(size);
    f.read(data.data(), size);
    return data;
}

static const char* get_string(const std::vector<char>& pool, uint32_t offset) {
    if (offset >= pool.size()) return "(invalid)";
    return pool.data() + offset;
}

template<typename T>
static std::vector<T> read_structs(const std::string& path) {
    auto data = read_file(path);
    size_t count = data.size() / sizeof(T);
    std::vector<T> result(count);
    memcpy(result.data(), data.data(), count * sizeof(T));
    return result;
}

// Round a float coordinate to an integer grid (1e5 ~ 1m precision)
static int to_grid(float v) {
    return (int)(v * 1e5f + (v >= 0 ? 0.5f : -0.5f));
}

// --- Canonical fingerprint builders ---
// Each function produces a string that uniquely identifies an item,
// independent of storage order.

// For a way: "name|sorted_node_coords"
// Nodes are kept in original order (path order matters for ways),
// but we also provide a sorted-set version for order-independent matching.
static std::string way_fingerprint(const std::string& name,
                                    const std::vector<std::pair<float,float>>& nodes) {
    // Use sorted coordinate set as the canonical form.
    // Two ways are "the same" if they have the same name and same set of nodes.
    std::vector<std::pair<int,int>> grid_nodes;
    grid_nodes.reserve(nodes.size());
    for (const auto& n : nodes) {
        grid_nodes.push_back({to_grid(n.first), to_grid(n.second)});
    }
    std::sort(grid_nodes.begin(), grid_nodes.end());

    std::string fp = name + "|" + std::to_string(nodes.size());
    for (const auto& g : grid_nodes) {
        fp += "|";
        fp += std::to_string(g.first);
        fp += ",";
        fp += std::to_string(g.second);
    }
    return fp;
}

// For an address: "grid_lat,grid_lng|housenumber|street"
static std::string addr_fingerprint(float lat, float lng,
                                     const std::string& housenumber,
                                     const std::string& street) {
    return std::to_string(to_grid(lat)) + "," + std::to_string(to_grid(lng)) +
           "|" + housenumber + "|" + street;
}

// For an admin polygon: "name|level|country|sorted_vertices"
static std::string admin_fingerprint(const std::string& name,
                                      uint8_t admin_level,
                                      uint16_t country_code,
                                      const std::vector<std::pair<float,float>>& vertices) {
    std::vector<std::pair<int,int>> grid_verts;
    grid_verts.reserve(vertices.size());
    for (const auto& v : vertices) {
        grid_verts.push_back({to_grid(v.first), to_grid(v.second)});
    }
    std::sort(grid_verts.begin(), grid_verts.end());

    std::string fp = name + "|" + std::to_string(admin_level) + "|" + std::to_string(country_code);
    for (const auto& g : grid_verts) {
        fp += "|";
        fp += std::to_string(g.first);
        fp += ",";
        fp += std::to_string(g.second);
    }
    return fp;
}

// For interpolation ways: "street|start|end|interp|sorted_nodes"
static std::string interp_fingerprint(const std::string& street,
                                       uint32_t start_number, uint32_t end_number,
                                       uint8_t interpolation,
                                       const std::vector<std::pair<float,float>>& nodes) {
    std::vector<std::pair<int,int>> grid_nodes;
    grid_nodes.reserve(nodes.size());
    for (const auto& n : nodes) {
        grid_nodes.push_back({to_grid(n.first), to_grid(n.second)});
    }
    std::sort(grid_nodes.begin(), grid_nodes.end());

    std::string fp = street + "|" + std::to_string(start_number) + "-" +
                     std::to_string(end_number) + "|" + std::to_string(interpolation);
    for (const auto& g : grid_nodes) {
        fp += "|";
        fp += std::to_string(g.first);
        fp += ",";
        fp += std::to_string(g.second);
    }
    return fp;
}

// --- Multiset comparison ---
// Compare two collections by their fingerprints. Reports missing/extra items.

static bool compare_multisets(const char* label,
                               const std::vector<std::string>& fps_a,
                               const std::vector<std::string>& fps_b) {
    // Build frequency maps
    std::unordered_map<std::string, int> freq_a, freq_b;
    for (const auto& fp : fps_a) freq_a[fp]++;
    for (const auto& fp : fps_b) freq_b[fp]++;

    // Find items in A but not in B (missing from B)
    size_t missing = 0;
    for (const auto& [fp, count] : freq_a) {
        int b_count = 0;
        auto it = freq_b.find(fp);
        if (it != freq_b.end()) b_count = it->second;
        if (count > b_count) missing += (count - b_count);
    }

    // Find items in B but not in A (extra in B)
    size_t extra = 0;
    for (const auto& [fp, count] : freq_b) {
        int a_count = 0;
        auto it = freq_a.find(fp);
        if (it != freq_a.end()) a_count = it->second;
        if (count > a_count) extra += (count - a_count);
    }

    size_t matched = fps_a.size() - missing;

    if (missing == 0 && extra == 0) {
        std::cout << "PASS: " << label << " (" << fps_a.size() << " items, all matched)" << std::endl;
        return true;
    } else {
        std::cerr << "FAIL: " << label << " — A=" << fps_a.size() << " B=" << fps_b.size()
                  << " matched=" << matched
                  << " only_in_A=" << missing
                  << " only_in_B=" << extra << std::endl;
        return false;
    }
}

// --- Decode + fingerprint functions ---

static std::vector<std::string> fingerprint_admin(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto polygons = read_structs<AdminPolygon>(dir + "/admin_polygons.bin");
    auto vertices = read_structs<NodeCoord>(dir + "/admin_vertices.bin");

    std::vector<std::string> fps;
    fps.reserve(polygons.size());
    for (const auto& p : polygons) {
        std::string name = get_string(strings, p.name_id);
        std::vector<std::pair<float,float>> verts;
        for (uint16_t i = 0; i < p.vertex_count; i++) {
            uint32_t vi = p.vertex_offset + i;
            if (vi < vertices.size()) {
                verts.push_back({vertices[vi].lat, vertices[vi].lng});
            }
        }
        fps.push_back(admin_fingerprint(name, p.admin_level, p.country_code, verts));
    }
    return fps;
}

static std::vector<std::string> fingerprint_ways(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto ways = read_structs<WayHeader>(dir + "/street_ways.bin");
    auto nodes = read_structs<NodeCoord>(dir + "/street_nodes.bin");

    std::vector<std::string> fps;
    fps.reserve(ways.size());
    for (const auto& w : ways) {
        std::string name = get_string(strings, w.name_id);
        std::vector<std::pair<float,float>> way_nodes;
        for (uint8_t i = 0; i < w.node_count; i++) {
            uint32_t ni = w.node_offset + i;
            if (ni < nodes.size()) {
                way_nodes.push_back({nodes[ni].lat, nodes[ni].lng});
            }
        }
        fps.push_back(way_fingerprint(name, way_nodes));
    }
    return fps;
}

static std::vector<std::string> fingerprint_addrs(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto points = read_structs<AddrPoint>(dir + "/addr_points.bin");

    std::vector<std::string> fps;
    fps.reserve(points.size());
    for (const auto& p : points) {
        std::string hn = get_string(strings, p.housenumber_id);
        std::string st = get_string(strings, p.street_id);
        fps.push_back(addr_fingerprint(p.lat, p.lng, hn, st));
    }
    return fps;
}

static std::vector<std::string> fingerprint_interps(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto interps = read_structs<InterpWay>(dir + "/interp_ways.bin");
    auto nodes = read_structs<NodeCoord>(dir + "/interp_nodes.bin");

    std::vector<std::string> fps;
    fps.reserve(interps.size());
    for (const auto& iw : interps) {
        std::string street = get_string(strings, iw.street_id);
        std::vector<std::pair<float,float>> way_nodes;
        for (uint8_t i = 0; i < iw.node_count; i++) {
            uint32_t ni = iw.node_offset + i;
            if (ni < nodes.size()) {
                way_nodes.push_back({nodes[ni].lat, nodes[ni].lng});
            }
        }
        fps.push_back(interp_fingerprint(street, iw.start_number, iw.end_number,
                                          iw.interpolation, way_nodes));
    }
    return fps;
}

// --- Semantic (cross-version) fingerprints ---
// Match by name/attributes only, ignoring coordinate precision differences

// Way: name + node_count only
static std::string way_semantic_fp(const std::string& name, size_t node_count) {
    return name + "|" + std::to_string(node_count);
}

// Address: housenumber + street + coarse grid coords (~100m precision)
// Centroid may differ slightly across code versions, so use coarse grid
static std::string addr_semantic_fp(float lat, float lng,
                                     const std::string& housenumber, const std::string& street) {
    int glat = (int)(lat * 1e3f + (lat >= 0 ? 0.5f : -0.5f));
    int glng = (int)(lng * 1e3f + (lng >= 0 ? 0.5f : -0.5f));
    return std::to_string(glat) + "," + std::to_string(glng) + "|" + housenumber + "|" + street;
}

// Admin: name + level
static std::string admin_semantic_fp(const std::string& name, uint8_t admin_level) {
    return name + "|" + std::to_string(admin_level);
}

static std::vector<std::string> semantic_fp_ways(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto ways = read_structs<WayHeader>(dir + "/street_ways.bin");

    std::vector<std::string> fps;
    fps.reserve(ways.size());
    for (const auto& w : ways) {
        std::string name = get_string(strings, w.name_id);
        fps.push_back(way_semantic_fp(name, w.node_count));
    }
    return fps;
}

static std::vector<std::string> semantic_fp_addrs(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto points = read_structs<AddrPoint>(dir + "/addr_points.bin");

    std::vector<std::string> fps;
    fps.reserve(points.size());
    for (const auto& p : points) {
        std::string hn = get_string(strings, p.housenumber_id);
        std::string st = get_string(strings, p.street_id);
        fps.push_back(addr_semantic_fp(hn, st));
    }
    return fps;
}

static std::vector<std::string> semantic_fp_admin(const std::string& dir) {
    auto strings = read_file(dir + "/strings.bin");
    auto polygons = read_structs<AdminPolygon>(dir + "/admin_polygons.bin");

    std::vector<std::string> fps;
    fps.reserve(polygons.size());
    for (const auto& p : polygons) {
        std::string name = get_string(strings, p.name_id);
        fps.push_back(admin_semantic_fp(name, p.admin_level));
    }
    return fps;
}

int main(int argc, char* argv[]) {
    if (argc < 3) {
        std::cerr << "Usage: compare_indexes <dir-A> <dir-B> [--semantic]" << std::endl;
        return 2;
    }

    std::string dir_a = argv[1];
    std::string dir_b = argv[2];
    bool semantic = (argc > 3 && std::string(argv[3]) == "--semantic");

    std::cout << "Comparing" << (semantic ? " (semantic/cross-version)" : " (exact)") << ":" << std::endl;
    std::cout << "  A: " << dir_a << std::endl;
    std::cout << "  B: " << dir_b << std::endl;
    std::cout << std::endl;

    bool all_pass = true;

    // Compare admin polygons
    {
        std::cout << "Loading admin polygons..." << std::flush;
        auto fps_a = semantic ? semantic_fp_admin(dir_a) : fingerprint_admin(dir_a);
        auto fps_b = semantic ? semantic_fp_admin(dir_b) : fingerprint_admin(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("admin_polygons", fps_a, fps_b))
            all_pass = false;
    }

    // Compare street ways
    {
        std::cout << "Loading street ways..." << std::flush;
        auto fps_a = semantic ? semantic_fp_ways(dir_a) : fingerprint_ways(dir_a);
        auto fps_b = semantic ? semantic_fp_ways(dir_b) : fingerprint_ways(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("street_ways", fps_a, fps_b))
            all_pass = false;
    }

    // Compare address points
    {
        std::cout << "Loading address points..." << std::flush;
        auto fps_a = semantic ? semantic_fp_addrs(dir_a) : fingerprint_addrs(dir_a);
        auto fps_b = semantic ? semantic_fp_addrs(dir_b) : fingerprint_addrs(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("address_points", fps_a, fps_b))
            all_pass = false;
    }

    // Compare interpolation ways
    {
        std::cout << "Loading interpolation ways..." << std::flush;
        auto fps_a = fingerprint_interps(dir_a);
        auto fps_b = fingerprint_interps(dir_b);
        std::cout << " done." << std::endl;
        if (!compare_multisets("interp_ways", fps_a, fps_b))
            all_pass = false;
    }

    // Compare cell index sizes (should be identical regardless of ordering)
    {
        auto ac = read_file(dir_a + "/admin_cells.bin");
        auto bc = read_file(dir_b + "/admin_cells.bin");
        if (ac == bc) {
            std::cout << "PASS: admin_cells.bin byte-identical (" << ac.size() << " bytes)" << std::endl;
        } else if (ac.size() == bc.size()) {
            std::cout << "INFO: admin_cells.bin same size (" << ac.size() << " bytes) but different content" << std::endl;
        } else {
            std::cerr << "FAIL: admin_cells.bin size differs (A=" << ac.size()
                      << " B=" << bc.size() << ")" << std::endl;
            all_pass = false;
        }

        auto gc_a = read_file(dir_a + "/geo_cells.bin");
        auto gc_b = read_file(dir_b + "/geo_cells.bin");
        if (gc_a.size() == gc_b.size()) {
            std::cout << "PASS: geo_cells.bin same size (" << gc_a.size() << " bytes)" << std::endl;
        } else {
            std::cerr << "FAIL: geo_cells.bin size differs (A=" << gc_a.size()
                      << " B=" << gc_b.size() << ")" << std::endl;
            all_pass = false;
        }
    }

    std::cout << std::endl;
    if (all_pass) {
        std::cout << "=== ALL COMPARISONS PASSED ===" << std::endl;
        return 0;
    } else {
        std::cout << "=== SOME COMPARISONS FAILED ===" << std::endl;
        return 1;
    }
}

-        fps.push_back(addr_semantic_fp(hn, st));

+        fps.push_back(addr_semantic_fp(p.lat, p.lng, hn, st));


Bash

cat /root/traccar-geocoder/builder/test/compare_indexes.cpp | ssh node3 "cat > /tmp/compare_indexes.cpp && g++ -O2 -std=c++17 -o /tmp/compare_indexes /tmp/compare_indexes.cpp && echo 'Compiled'" 2>&1

Compile updated comparison tool


ssh node3 "/tmp/compare_indexes /tmp/de-baseline /tmp/de-parallel-fixed --semantic 2>&1"

Semantic comparison with coarse grid addresses


Error: Exit code 1
Comparing (semantic/cross-version):
  A: /tmp/de-baseline
  B: /tmp/de-parallel-fixed

Loading admin polygons... done.
FAIL: admin_polygons — A=43221 B=43178 matched=43170 only_in_A=51 only_in_B=8
Loading street ways... done.
FAIL: street_ways — A=3298645 B=3312673 matched=3298645 only_in_A=0 only_in_B=14028
Loading address points... done.
FAIL: address_points — A=19951156 B=19951156 matched=3960921 only_in_A=15990235 only_in_B=15990235
Loading interpolation ways... done.
PASS: interp_ways (78 items, all matched)
INFO: admin_cells.bin same size (73236 bytes) but different content
FAIL: geo_cells.bin size differs (A=186220544 B=195412000)

=== SOME COMPARISONS FAILED ===


The address matching is still failing at 80%. Let me check if the string data is actually correct by comparing specific addresses.


ssh node3 "cat > /tmp/debug_addr2.cpp << 'EOFCPP'
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iostream>
#include <vector>
#include <map>

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

static std::vector<char> read_file(const std::string& path) {
    std::ifstream f(path, std::ios::binary | std::ios::ate);
    if (!f) return {};
    auto sz = f.tellg(); f.seekg(0);
    std::vector<char> d(sz); f.read(d.data(), sz); return d;
}

int main(int argc, char* argv[]) {
    auto load = [](const std::string& dir) {
        auto strings = read_file(dir + \"/strings.bin\");
        auto data = read_file(dir + \"/addr_points.bin\");
        auto* pts = (AddrPoint*)data.data();
        size_t count = data.size() / sizeof(AddrPoint);

        // Count unique hn|street combos
        std::map<std::string, int> combos;
        int bad_str = 0;
        for (size_t i = 0; i < count; i++) {
            const char* hn = (pts[i].housenumber_id < strings.size()) ? strings.data() + pts[i].housenumber_id : nullptr;
            const char* st = (pts[i].street_id < strings.size()) ? strings.data() + pts[i].street_id : nullptr;
            if (!hn || !st) { bad_str++; continue; }
            // Check for embedded nulls or weird chars
            std::string key = std::string(hn) + \"|\" + std::string(st);
            combos[key]++;
        }
        return std::make_pair(combos, bad_str);
    };

    printf(\"Loading A (%s)...\\n\", argv[1]);
    auto [a, bad_a] = load(argv[1]);
    printf(\"  Unique combos: %zu, bad strings: %d\\n\", a.size(), bad_a);

    printf(\"Loading B (%s)...\\n\", argv[2]);
    auto [b, bad_b] = load(argv[2]);
    printf(\"  Unique combos: %zu, bad strings: %d\\n\", b.size(), bad_b);

    // Compare multisets
    size_t matched = 0, only_a = 0, only_b = 0;
    for (auto& [k, cnt] : a) {
        auto it = b.find(k);
        int bc = (it != b.end()) ? it->second : 0;
        int m = std::min(cnt, bc);
        matched += m;
        only_a += (cnt - m);
    }
    for (auto& [k, cnt] : b) {
        auto it = a.find(k);
        int ac = (it != a.end()) ? it->second : 0;
        if (cnt > ac) only_b += (cnt - ac);
    }
    printf(\"\\nString-only multiset: matched=%zu only_A=%zu only_B=%zu\\n\", matched, only_a, only_b);

    // Show some only-in-A combos
    int shown = 0;
    for (auto& [k, cnt] : a) {
        auto it = b.find(k);
        int bc = (it != b.end()) ? it->second : 0;
        if (cnt > bc && shown < 5) {
            printf(\"  Only_A: '%s' (A=%d B=%d)\\n\", k.c_str(), cnt, bc);
            shown++;
        }
    }
    shown = 0;
    for (auto& [k, cnt] : b) {
        auto it = a.find(k);
        int ac = (it != a.end()) ? it->second : 0;
        if (cnt > ac && shown < 5) {
            printf(\"  Only_B: '%s' (B=%d A=%d)\\n\", k.c_str(), cnt, ac);
            shown++;
        }
    }

    return 0;
}
EOFCPP
g++ -O2 -std=c++17 -o /tmp/debug_addr2 /tmp/debug_addr2.cpp && /tmp/debug_addr2 /tmp/de-baseline /tmp/de-parallel-fixed" 2>&1

Deep compare address strings between baseline and new code


Command running in background with ID: b55l27czw. Output is being written to: /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b55l27czw.output


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b55l27czw.output 2>/dev/null

Check address debug output


Loading A (/tmp/de-baseline)...
  Unique combos: 9511659, bad strings: 0
Loading B (/tmp/de-parallel-fixed)...
  Unique combos: 10338120, bad strings: 0

String-only multiset: matched=3960926 only_A=15990230 only_B=15990230
  Only_A: '!21|Deberndorfer Straße' (A=1 B=0)
  Only_A: '!22b|Karlstraße' (A=1 B=0)
  Only_A: '!23a|Weidenstraße' (A=1 B=0)
  Only_A: '!32|Saulsiek' (A=1 B=0)
  Only_A: '!33|Stillestraße' (A=1 B=0)
  Only_B: '!21Deberndorfer Straße|!21Deberndorfer Straße' (B=1 A=0)
  Only_B: '!22bKarlstraße|!22bKarlstraße' (B=1 A=0)
  Only_B: '!23aWeidenstraße|!23aWeidenstraße' (B=1 A=0)
  Only_B: '!32Saulsiek|!32Saulsiek' (B=1 A=0)
  Only_B: '!33Stillestraße|!33Stillestraße' (B=1 A=0)


Found it! The new code's building addresses have corrupted strings. Look at the pattern:

- **Baseline**: `hn='!21'  st='Deberndorfer Straße'`
- **New code**: `hn='!21Deberndorfer Straße'  st='!21Deberndorfer Straße'`

The housenumber and street are concatenated together in the new code, and both fields contain the same concatenated string. This is the `addr_strings` parsing bug — the `\0` separator in the `std::string` isn't being handled correctly during merge.


  2214→                            }
  2215→                        } dense_loc_handler{index};
  2216→
  2217→                        osmium::io::Reader reader_rels{input_file};
  2218→                        osmium::apply(reader_rels, dense_loc_handler,
  2219→                            mp_manager.handler([&handler](osmium::memory::Buffer&& buffer) {
  2220→                                osmium::apply(buffer, handler);
  2221→                            }));
  2222→                        reader_rels.close();
  2223→                    }
  2224→                }
  2225→
  2226→                // Merge thread-local way/interp data into main ParsedData
  2227→                std::cerr << "  Merging thread-local data..." << std::endl;
  2228→                uint64_t total_ways = 0, total_building_addrs = 0, total_interps = 0;

                            }
                        } dense_loc_handler{index};

                        osmium::io::Reader reader_rels{input_file};
                        osmium::apply(reader_rels, dense_loc_handler,
                            mp_manager.handler([&handler](osmium::memory::Buffer&& buffer) {
                                osmium::apply(buffer, handler);
                            }));
                        reader_rels.close();
                    }
                }

                // Merge thread-local way/interp data into main ParsedData
                std::cerr << "  Merging thread-local data..." << std::endl;
                uint64_t total_ways = 0, total_building_addrs = 0, total_interps = 0;