Bash

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 -3 && rm -rf /tmp/de-diag2 && mkdir -p /tmp/de-diag2 && /tmp/traccar-geocoder/build/build-index /tmp/de-diag2 ~/pbf/germany.osm.pbf --parallel-admin 2>&1 | grep 'DEBUG'" 2>&1

Build and run with detailed node ID logging


<persisted-output>
Output too large (36.9KB). Full output saved to: /root/.claude/projects/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tool-results/baqd16t18.txt

Preview (first 2KB):
[ 50%] Building CXX object CMakeFiles/build-index.dir/src/build_index.cpp.o
[100%] Linking CXX executable build-index
[100%] Built target build-index
  DEBUG 'Landkreis Osterholz': ways: [w29412859 first=323750810 last=323260461 n=322] [w72473011 first=323260461 last=323598813 n=235] [w51720317 first=323598813 last=31117746 n=23] [w69332136 first=31117746 last=323767016 n=67] [w447961326 first=323767016 last=2160967269 n=78] [w29407675 first=2160967269 last=10563758401 n=13] [w69352922 first=10563758401 last=833938534 n=11] [w71136849 first=833938534 last=292033524 n=30] [w71136843 first=292033524 last=323349999 n=43] [w1298342079 first=323349999 last=323331641 n=6] [w69482196 first=323331641 last=830637332 n=3] [w51720493 first=830637332 last=323498028 n=6] [w69557921 first=323498028 last=323443248 n=31] [w29417069 first=323443248 last=323503515 n=53] [w141873992 first=323503515 last=3587234602 n=3] [w352933032 first=3587234602 last=832873536 n=5] [w764575455 first=832873536 last=323276538 n=6] [w69601332 first=323276538 last=323648270 n=42] [w447640041 first=323648270 last=323878924 n=40] [w71155863 first=323878924 last=324067048 n=11] [w69662687 first=324067048 last=295335518 n=55] [w71339903 first=295335518 last=323705711 n=106] [w765172621 first=323705711 last=847320188 n=16] [w71176916 first=847320188 last=323617913 n=5] [w69680572 first=323617913 last=323605788 n=8] [w71176905 first=323605788 last=323504504 n=61] [w29412743 first=697163499 last=323504504 n=25] [w286564707 first=697163499 last=2902496348 n=42] [w1478839070 first=2902496348 last=3313112100 n=6] [w1478839072 first=3313112063 last=3313112100 n=6] [w1478839073 first=3313111920 last=3313112063 n=8] [w107411523 first=3313111920 last=1234535661 n=266] [w107411456 first=1234535661 last=323334310 n=492] [w38501694 first=323334310 last=323522467 n=171] [w1478839079 first=323573550 last=323522467 n=22] [w1478839080 first=323573550 last=323772516 n=117] [w1478839082 first=323772516 last=323484750 n=70] [w
...
</persisted-output>


Let me analyze the 'Landkreis Osterholz' case — the chain breaks somewhere.


ssh node3 "grep 'DEBUG.*Siedelsbrunn' /tmp/de-diag2-log.txt 2>/dev/null" 2>&1
# The log was piped, let me re-run with just one small example
ssh node3 "cat /root/.claude/projects/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tool-results/baqd16t18.txt 2>/dev/null" | head -1

Check for output


(Bash completed with no output)


     1→[ 50%] Building CXX object CMakeFiles/build-index.dir/src/build_index.cpp.o
     2→[100%] Linking CXX executable build-index
     3→[100%] Built target build-index
     4→  DEBUG 'Landkreis Osterholz': ways: [w29412859 first=323750810 last=323260461 n=322] [w72473011 first=323260461 last=323598813 n=235] [w51720317 first=323598813 last=31117746 n=23] [w69332136 first=31117746 last=323767016 n=67] [w447961326 first=323767016 last=2160967269 n=78] [w29407675 first=2160967269 last=10563758401 n=13] [w69352922 first=10563758401 last=833938534 n=11] [w71136849 first=833938534 last=292033524 n=30] [w71136843 first=292033524 last=323349999 n=43] [w1298342079 first=323349999 last=323331641 n=6] [w69482196 first=323331641 last=830637332 n=3] [w51720493 first=830637332 last=323498028 n=6] [w69557921 first=323498028 last=323443248 n=31] [w29417069 first=323443248 last=323503515 n=53] [w141873992 first=323503515 last=3587234602 n=3] [w352933032 first=3587234602 last=832873536 n=5] [w764575455 first=832873536 last=323276538 n=6] [w69601332 first=323276538 last=323648270 n=42] [w447640041 first=323648270 last=323878924 n=40] [w71155863 first=323878924 last=324067048 n=11] [w69662687 first=324067048 last=295335518 n=55] [w71339903 first=295335518 last=323705711 n=106] [w765172621 first=323705711 last=847320188 n=16] [w71176916 first=847320188 last=323617913 n=5] [w69680572 first=323617913 last=323605788 n=8] [w71176905 first=323605788 last=323504504 n=61] [w29412743 first=697163499 last=323504504 n=25] [w286564707 first=697163499 last=2902496348 n=42] [w1478839070 first=2902496348 last=3313112100 n=6] [w1478839072 first=3313112063 last=3313112100 n=6] [w1478839073 first=3313111920 last=3313112063 n=8] [w107411523 first=3313111920 last=1234535661 n=266] [w107411456 first=1234535661 last=323334310 n=492] [w38501694 first=323334310 last=323522467 n=171] [w1478839079 first=323573550 last=323522467 n=22] [w1478839080 first=323573550 last=323772516 n=117] [w1478839082 first=323772516 last=323484750 n=70] [w1478839084 first=13566850757 last=323605164 n=31] [w107411497 first=323484750 last=13566850757 n=271] [w1478839088 first=323605164 last=323540681 n=70] [w233098511 first=13567068870 last=324151463 n=127] [w58507038 first=324151463 last=725153187 n=10] [w1478872559 first=324055363 last=725153187 n=88] [w75793403 first=324055363 last=323920367 n=82] [w58507046 first=323920367 last=725153198 n=90] [w107309856 first=725153198 last=323457906 n=169] [w107309857 first=323457906 last=323468038 n=86] [w107309880 first=323468038 last=323416154 n=40] [w29416968 first=323416154 last=323537109 n=57] [w29413056 first=323537109 last=323277627 n=5] [w58486912 first=323277627 last=663702748 n=41] [w52071951 first=323506329 last=663702748 n=3] [w62125583 first=323552278 last=323506329 n=30] [w251939260 first=323820363 last=323552278 n=28] [w65640701 first=323496348 last=323820363 n=11] [w362100175 first=323434973 last=323496348 n=73] [w52057046 first=323653212 last=323434973 n=13] [w62125617 first=324013476 last=323653212 n=43] [w62125592 first=323728238 last=324013476 n=31] [w52042229 first=323394398 last=323728238 n=14] [w29413297 first=323394398 last=323345890 n=37] [w51741362 first=323345890 last=323750810 n=49]
     5→  DEBUG 'Osterholz-Scharmbeck': ways: [w25568380 first=247422885 last=260525115 n=3] [w1224939094 first=247422885 last=11359610117 n=12] [w1224939095 first=11359610117 last=663831729 n=3] [w38503151 first=663831729 last=1457110001 n=17] [w38503149 first=31088096 last=1457110001 n=34] [w38503261 first=295331649 last=31088096 n=31] [w38503264 first=31088056 last=295331649 n=29] [w38502528 first=31088009 last=31088056 n=52] [w38502527 first=31088009 last=270344496 n=96] [w24705509 first=270344299 last=270344496 n=30] [w24879171 first=270344299 last=268525230 n=41] [w29416966 first=268525230 last=4831691597 n=34] [w491002746 first=4831691597 last=4831691598 n=2] [w491002745 first=4831691598 last=268232077 n=25] [w38532504 first=268232077 last=4311038233 n=5] [w431875555 first=4311038233 last=3238658160 n=2] [w431875556 first=3238658160 last=270507198 n=10] [w58487319 first=272366376 last=270507198 n=21] [w38532505 first=324151463 last=272366376 n=26] [w233098511 first=13567068870 last=324151463 n=127] [w1478839088 first=323605164 last=323540681 n=70] [w1478839084 first=13566850757 last=323605164 n=31] [w107411497 first=323484750 last=13566850757 n=271] [w1478839082 first=323772516 last=323484750 n=70] [w1478839080 first=323573550 last=323772516 n=117] [w38501684 first=323573550 last=455502852 n=35] [w620736412 first=455502852 last=660120403 n=4] [w247541866 first=298840717 last=660120403 n=15] [w38501681 first=462958591 last=298840717 n=11] [w24949450 first=462958591 last=260525115 n=17]

[ 50%] Building CXX object CMakeFiles/build-index.dir/src/build_index.cpp.o
[100%] Linking CXX executable build-index
[100%] Built target build-index
  DEBUG 'Landkreis Osterholz': ways: [w29412859 first=323750810 last=323260461 n=322] [w72473011 first=323260461 last=323598813 n=235] [w51720317 first=323598813 last=31117746 n=23] [w69332136 first=31117746 last=323767016 n=67] [w447961326 first=323767016 last=2160967269 n=78] [w29407675 first=2160967269 last=10563758401 n=13] [w69352922 first=10563758401 last=833938534 n=11] [w71136849 first=833938534 last=292033524 n=30] [w71136843 first=292033524 last=323349999 n=43] [w1298342079 first=323349999 last=323331641 n=6] [w69482196 first=323331641 last=830637332 n=3] [w51720493 first=830637332 last=323498028 n=6] [w69557921 first=323498028 last=323443248 n=31] [w29417069 first=323443248 last=323503515 n=53] [w141873992 first=323503515 last=3587234602 n=3] [w352933032 first=3587234602 last=832873536 n=5] [w764575455 first=832873536 last=323276538 n=6] [w69601332 first=323276538 last=323648270 n=42] [w447640041 first=323648270 last=323878924 n=40] [w71155863 first=323878924 last=324067048 n=11] [w69662687 first=324067048 last=295335518 n=55] [w71339903 first=295335518 last=323705711 n=106] [w765172621 first=323705711 last=847320188 n=16] [w71176916 first=847320188 last=323617913 n=5] [w69680572 first=323617913 last=323605788 n=8] [w71176905 first=323605788 last=323504504 n=61] [w29412743 first=697163499 last=323504504 n=25] [w286564707 first=697163499 last=2902496348 n=42] [w1478839070 first=2902496348 last=3313112100 n=6] [w1478839072 first=3313112063 last=3313112100 n=6] [w1478839073 first=3313111920 last=3313112063 n=8] [w107411523 first=3313111920 last=1234535661 n=266] [w107411456 first=1234535661 last=323334310 n=492] [w38501694 first=323334310 last=323522467 n=171] [w1478839079 first=323573550 last=323522467 n=22] [w1478839080 first=323573550 last=323772516 n=117] [w1478839082 first=323772516 last=323484750 n=70] [w1478839084 first=13566850757 last=323605164 n=31] [w107411497 first=323484750 last=13566850757 n=271] [w1478839088 first=323605164 last=323540681 n=70] [w233098511 first=13567068870 last=324151463 n=127] [w58507038 first=324151463 last=725153187 n=10] [w1478872559 first=324055363 last=725153187 n=88] [w75793403 first=324055363 last=323920367 n=82] [w58507046 first=323920367 last=725153198 n=90] [w107309856 first=725153198 last=323457906 n=169] [w107309857 first=323457906 last=323468038 n=86] [w107309880 first=323468038 last=323416154 n=40] [w29416968 first=323416154 last=323537109 n=57] [w29413056 first=323537109 last=323277627 n=5] [w58486912 first=323277627 last=663702748 n=41] [w52071951 first=323506329 last=663702748 n=3] [w62125583 first=323552278 last=323506329 n=30] [w251939260 first=323820363 last=323552278 n=28] [w65640701 first=323496348 last=323820363 n=11] [w362100175 first=323434973 last=323496348 n=73] [w52057046 first=323653212 last=323434973 n=13] [w62125617 first=324013476 last=323653212 n=43] [w62125592 first=323728238 last=324013476 n=31] [w52042229 first=323394398 last=323728238 n=14] [w29413297 first=323394398 last=323345890 n=37] [w51741362 first=323345890 last=323750810 n=49]
  DEBUG 'Osterholz-Scharmbeck': ways: [w25568380 first=247422885 last=260525115 n=3] [w1224939094 first=247422885 last=11359610117 n=12] [w1224939095 first=11359610117 last=663831729 n=3] [w38503151 first=663831729 last=1457110001 n=17] [w38503149 first=31088096 last=1457110001 n=34] [w38503261 first=295331649 last=31088096 n=31] [w38503264 first=31088056 last=295331649 n=29] [w38502528 first=31088009 last=31088056 n=52] [w38502527 first=31088009 last=270344496 n=96] [w24705509 first=270344299 last=270344496 n=30] [w24879171 first=270344299 last=268525230 n=41] [w29416966 first=268525230 last=4831691597 n=34] [w491002746 first=4831691597 last=4831691598 n=2] [w491002745 first=4831691598 last=268232077 n=25] [w38532504 first=268232077 last=4311038233 n=5] [w431875555 first=4311038233 last=3238658160 n=2] [w431875556 first=3238658160 last=270507198 n=10] [w58487319 first=272366376 last=270507198 n=21] [w38532505 first=324151463 last=272366376 n=26] [w233098511 first=13567068870 last=324151463 n=127] [w1478839088 first=323605164 last=323540681 n=70] [w1478839084 first=13566850757 last=323605164 n=31] [w107411497 first=323484750 last=13566850757 n=271] [w1478839082 first=323772516 last=323484750 n=70] [w1478839080 first=323573550 last=323772516 n=117] [w38501684 first=323573550 last=455502852 n=35] [w620736412 first=455502852 last=660120403 n=4] [w247541866 first=298840717 last=660120403 n=15] [w38501681 first=462958591 last=298840717 n=11] [w24949450 first=462958591 last=260525115 n=17]


12:  DEBUG 'Siedelsbrunn': ways: [w176483465 first=1870000276 last=676750500 n=216] [w176483937 first=1870000353 last=12620891323 n=63] [w176482253 first=1870000353 last=676750500 n=7]


Siedelsbrunn has 3 ways:
- w176483465: first=1870000276 last=676750500 (216 coords)
- w176483937: first=1870000353 last=12620891323 (63 coords)
- w176482253: first=1870000353 last=676750500 (7 coords)

The chain:
- Way 1 ends at 676750500
- Way 3 starts at 1870000353 and ends at 676750500 (can be reversed)
- Way 2 starts at 1870000353 and ends at 12620891323

So: Way1(1870000276→676750500) + Way3_reversed(676750500→1870000353) + Way2(1870000353→12620891323)
Ring_first = 1870000276, ring_last = 12620891323. These are NOT equal — the ring can't close!

The ring needs node 1870000276 == 12620891323, but they're different. **Way 2's last node (12620891323) doesn't connect back to way 1's first node (1870000276)**. This means either the relation data is broken, or we're missing a way that connects 12620891323 to 1870000276.

But wait — the diff says "3 found, 0 missing". All 3 outer member ways are present. So the OSM relation data might reference additional ways with different roles, or the ring closure goes through an "inner" way?

Actually, looking at the node IDs — `12620891323` and `1870000276` — osmium's assembler might handle this differently. Let me check if these could actually be the same node (one might be a newer version). Or the relation might have additional members with roles we're ignoring.

Let me check if the relation has non-outer members.


No matches found


129:struct CollectedRelation {
259:    std::vector<CollectedRelation> collected_relations;
891:struct RelationCollector : public osmium::handler::Handler {
892:    std::vector<CollectedRelation>& relations;
894:    explicit RelationCollector(std::vector<CollectedRelation>& r) : relations(r) {}
937:        CollectedRelation cr;
1828:            RelationCollector rel_collector(data.collected_relations);
1842:                std::cerr << "  Collected " << data.collected_relations.size()
1962:                for (const auto& rel : data.collected_relations) {
2301:                              << data.collected_relations.size() << " relations, "
2322:                                if (i >= data.collected_relations.size()) break;
2324:                                const auto& rel = data.collected_relations[i];
2395:                              << data.collected_relations.size() << " relations." << std::endl;
2398:                    data.collected_relations.clear();
2399:                    data.collected_relations.shrink_to_fit();


   891→struct RelationCollector : public osmium::handler::Handler {
   892→    std::vector<CollectedRelation>& relations;
   893→
   894→    explicit RelationCollector(std::vector<CollectedRelation>& r) : relations(r) {}
   895→
   896→    void relation(const osmium::Relation& rel) {
   897→        const char* boundary = rel.tags()["boundary"];
   898→        if (!boundary) return;
   899→
   900→        bool is_admin = (std::strcmp(boundary, "administrative") == 0);
   901→        bool is_postal = (std::strcmp(boundary, "postal_code") == 0);
   902→        if (!is_admin && !is_postal) return;
   903→
   904→        uint8_t admin_level = 0;
   905→        if (is_admin) {
   906→            const char* level_str = rel.tags()["admin_level"];
   907→            if (!level_str) return;
   908→            admin_level = static_cast<uint8_t>(std::atoi(level_str));
   909→            if (admin_level < 2 || admin_level > 10) return;
   910→        } else {
   911→            admin_level = 11; // use 11 for postal codes
   912→        }
   913→        if (kMaxAdminLevel > 0 && admin_level > kMaxAdminLevel) return;
   914→
   915→        const char* name = rel.tags()["name"];
   916→        if (!name && is_admin) return;
   917→
   918→        // For postal codes, use postal_code tag as name
   919→        std::string name_str;
   920→        if (is_postal) {
   921→            const char* postal_code = rel.tags()["postal_code"];
   922→            if (!postal_code) postal_code = name;
   923→            if (!postal_code) return;
   924→            name_str = postal_code;
   925→        } else {
   926→            name_str = name;
   927→        }
   928→
   929→        // Extract country code for level 2 boundaries
   930→        std::string country_code;
   931→        if (admin_level == 2) {
   932→            const char* cc = rel.tags()["ISO3166-1:alpha2"];
   933→            if (cc) country_code = cc;
   934→        }
   935→
   936→        // Collect member way IDs and roles
   937→        CollectedRelation cr;
   938→        cr.id = rel.id();
   939→        cr.admin_level = admin_level;
   940→        cr.name = std::move(name_str);
   941→        cr.country_code = std::move(country_code);
   942→        cr.is_postal = is_postal;
   943→
   944→        for (const auto& member : rel.members()) {
   945→            if (member.type() == osmium::item_type::way) {
   946→                cr.members.emplace_back(member.ref(), member.role());
   947→            }
   948→        }
   949→
   950→        if (!cr.members.empty()) {

struct RelationCollector : public osmium::handler::Handler {
    std::vector<CollectedRelation>& relations;

    explicit RelationCollector(std::vector<CollectedRelation>& r) : relations(r) {}

    void relation(const osmium::Relation& rel) {
        const char* boundary = rel.tags()["boundary"];
        if (!boundary) return;

        bool is_admin = (std::strcmp(boundary, "administrative") == 0);
        bool is_postal = (std::strcmp(boundary, "postal_code") == 0);
        if (!is_admin && !is_postal) return;

        uint8_t admin_level = 0;
        if (is_admin) {
            const char* level_str = rel.tags()["admin_level"];
            if (!level_str) return;
            admin_level = static_cast<uint8_t>(std::atoi(level_str));
            if (admin_level < 2 || admin_level > 10) return;
        } else {
            admin_level = 11; // use 11 for postal codes
        }
        if (kMaxAdminLevel > 0 && admin_level > kMaxAdminLevel) return;

        const char* name = rel.tags()["name"];
        if (!name && is_admin) return;

        // For postal codes, use postal_code tag as name
        std::string name_str;
        if (is_postal) {
            const char* postal_code = rel.tags()["postal_code"];
            if (!postal_code) postal_code = name;
            if (!postal_code) return;
            name_str = postal_code;
        } else {
            name_str = name;
        }

        // Extract country code for level 2 boundaries
        std::string country_code;
        if (admin_level == 2) {
            const char* cc = rel.tags()["ISO3166-1:alpha2"];
            if (cc) country_code = cc;
        }

        // Collect member way IDs and roles
        CollectedRelation cr;
        cr.id = rel.id();
        cr.admin_level = admin_level;
        cr.name = std::move(name_str);
        cr.country_code = std::move(country_code);
        cr.is_postal = is_postal;

        for (const auto& member : rel.members()) {
            if (member.type() == osmium::item_type::way) {
                cr.members.emplace_back(member.ref(), member.role());
            }
        }

        if (!cr.members.empty()) {


I see — the RelationCollector stores ALL way members regardless of role. But `assemble_outer_rings` only uses members with role "outer" or empty. The Siedelsbrunn relation might have members with role "" (empty), and the ring might depend on all members (not just outer ones) to close.

But looking at the Siedelsbrunn data: 3 members, all collected. The `assemble_outer_rings` filters for "outer" or empty role, and found 3 ways — so all 3 have outer/empty role. The chain simply doesn't close: node 1870000276 ≠ 12620891323.

This means the **OSM relation data itself has these two distinct nodes at the boundary gap**. Osmium's assembler likely handles this differently — perhaps it uses coordinate matching as a fallback when node IDs don't match, or it handles the relation differently.

Let me check if these two node IDs might have the same coordinates (they could be a split node).


ssh node3 "cat > /tmp/check_nodes.cpp << 'EOFCPP'
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iostream>
#include <vector>
#include <set>
#include <osmium/io/pbf_input.hpp>
#include <osmium/handler.hpp>
#include <osmium/visitor.hpp>
#include <osmium/osm/node.hpp>

int main(int argc, char* argv[]) {
    // Check specific node IDs
    std::set<int64_t> target_ids = {1870000276, 12620891323, 676750500, 1870000353};

    osmium::io::Reader reader{argv[1], osmium::osm_entity_bits::node};
    while (auto buffer = reader.read()) {
        for (const auto& item : buffer) {
            if (item.type() == osmium::item_type::node) {
                const auto& node = static_cast<const osmium::Node&>(item);
                if (target_ids.count(node.id())) {
                    printf(\"Node %ld: lat=%.10f lng=%.10f\\n\", (long)node.id(), node.location().lat(), node.location().lon());
                }
            }
        }
    }
    reader.close();
    return 0;
}
EOFCPP
cd /tmp/traccar-geocoder/build && g++ -O2 -std=c++17 -I/usr/include -o /tmp/check_nodes /tmp/check_nodes.cpp -L/usr/lib -lz -lbz2 -lexpat -losmpbf -lprotobuf -lpthread 2>&1" 2>&1

Build node checker tool


Error: Exit code 1
/usr/bin/ld: cannot find -losmpbf: No such file or directory
/usr/bin/ld: cannot find -lprotobuf: No such file or directory
collect2: error: ld returned 1 exit status


ssh node3 "cat /tmp/traccar-geocoder/build/CMakeCache.txt | grep -E 'LINK_LIB|ZLIB|PROTO'" 2>&1

Find link flags from cmake


ZLIB_INCLUDE_DIR:PATH=/usr/include
ZLIB_LIBRARY_DEBUG:FILEPATH=ZLIB_LIBRARY_DEBUG-NOTFOUND
ZLIB_LIBRARY_RELEASE:FILEPATH=/usr/lib/x86_64-linux-gnu/libz.so
//Details about finding ZLIB
FIND_PACKAGE_MESSAGE_DETAILS_ZLIB:INTERNAL=[/usr/lib/x86_64-linux-gnu/libz.so][/usr/include][c ][v1.3.1()]
//ADVANCED property for variable: ZLIB_INCLUDE_DIR
ZLIB_INCLUDE_DIR-ADVANCED:INTERNAL=1
//ADVANCED property for variable: ZLIB_LIBRARY_DEBUG
ZLIB_LIBRARY_DEBUG-ADVANCED:INTERNAL=1
//ADVANCED property for variable: ZLIB_LIBRARY_RELEASE
ZLIB_LIBRARY_RELEASE-ADVANCED:INTERNAL=1


ssh node3 "cat /tmp/traccar-geocoder/build/CMakeFiles/build-index.dir/link.txt" 2>&1

Get linker flags


/usr/bin/c++ -Wl,--dependency-file=CMakeFiles/build-index.dir/link.d "CMakeFiles/build-index.dir/src/build_index.cpp.o" -o build-index  -ls2 /usr/lib/x86_64-linux-gnu/libz.so /usr/lib/x86_64-linux-gnu/libbz2.so /usr/lib/x86_64-linux-gnu/libexpat.so


ssh node3 "g++ -O2 -std=c++17 -o /tmp/check_nodes /tmp/check_nodes.cpp -ls2 /usr/lib/x86_64-linux-gnu/libz.so /usr/lib/x86_64-linux-gnu/libbz2.so /usr/lib/x86_64-linux-gnu/libexpat.so -lpthread 2>&1 && /tmp/check_nodes ~/pbf/germany.osm.pbf" 2>&1

Build and run node checker


Node 676750500: lat=49.5430667000 lng=8.8018147000
Node 1870000276: lat=49.5576001000 lng=8.8074189000
Node 1870000353: lat=49.5435522000 lng=8.7992825000
Node 12620891323: lat=49.5576001000 lng=8.8074189000


**Node 1870000276 and 12620891323 have IDENTICAL coordinates** (lat=49.5576001, lng=8.8074189)! They're the same physical location — a split/replaced node in OSM. The ring stitcher fails because it matches by node ID, but these are different IDs at the same location.

This is the root cause: when OSM editors split or replace nodes, the new node gets a different ID but the same coordinates. The ring stitcher needs a **coordinate-based fallback** when node ID matching fails.


Edit

    // Build adjacency: node_id -> list of (way_index, is_last_node)
    // Each way has two endpoints: first_node_id and last_node_id
    std::unordered_map<int64_t, std::vector<std::pair<size_t, bool>>> node_to_ways;
    for (size_t i = 0; i < outer_ways.size(); i++) {
        const auto* g = outer_ways[i].geom;
        node_to_ways[g->first_node_id].push_back({i, false});
        node_to_ways[g->last_node_id].push_back({i, true});
    }

    // Build adjacency: node_id -> list of (way_index, is_last_node)
    // Each way has two endpoints: first_node_id and last_node_id
    std::unordered_map<int64_t, std::vector<std::pair<size_t, bool>>> node_to_ways;
    for (size_t i = 0; i < outer_ways.size(); i++) {
        const auto* g = outer_ways[i].geom;
        node_to_ways[g->first_node_id].push_back({i, false});
        node_to_ways[g->last_node_id].push_back({i, true});
    }

    // Build coordinate-based fallback adjacency for split/replaced nodes
    // Key: rounded lat,lng pair → list of node IDs at that location
    auto coord_key = [](double lat, double lng) -> int64_t {
        // Round to ~1cm precision to match identical coordinates
        int32_t ilat = static_cast<int32_t>(lat * 1e7 + (lat >= 0 ? 0.5 : -0.5));
        int32_t ilng = static_cast<int32_t>(lng * 1e7 + (lng >= 0 ? 0.5 : -0.5));
        return (static_cast<int64_t>(ilat) << 32) | static_cast<uint32_t>(ilng);
    };
    std::unordered_map<int64_t, std::vector<int64_t>> coord_to_nids;
    for (size_t i = 0; i < outer_ways.size(); i++) {
        const auto* g = outer_ways[i].geom;
        if (!g->coords.empty()) {
            auto& first = g->coords.front();
            auto& last = g->coords.back();
            coord_to_nids[coord_key(first.first, first.second)].push_back(g->first_node_id);
            coord_to_nids[coord_key(last.first, last.second)].push_back(g->last_node_id);
        }
    }
    // Map node IDs that share coordinates (split/replaced nodes)
    std::unordered_map<int64_t, std::vector<int64_t>> nid_aliases;
    for (auto& [ck, nids] : coord_to_nids) {
        if (nids.size() <= 1) continue;
        // Deduplicate
        std::sort(nids.begin(), nids.end());
        nids.erase(std::unique(nids.begin(), nids.end()), nids.end());
        if (nids.size() <= 1) continue;
        for (auto nid : nids) {
            nid_aliases[nid] = nids;
        }
    }