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-parallel-split && mkdir -p /tmp/de-parallel-split && /tmp/traccar-geocoder/build/build-index /tmp/de-parallel-split ~/pbf/germany.osm.pbf --parallel-admin 2>&1 | grep -E 'polygon|Done|Parallel admin'" 2>&1

Build and run with split-at-shared-nodes approach


[ 50%] Building CXX object CMakeFiles/build-index.dir/src/build_index.cpp.o
[100%] Linking CXX executable build-index
[100%] Built target build-index
  Assembling admin polygons in parallel (40080 relations, 112858 way geometries)...
  Parallel admin assembly complete: 43193 polygon rings from 40080 relations.
Done reading:
  43193 admin polygon rings
Waiting for admin polygon S2 covering to complete...
Admin polygon S2 covering complete (6103 cells).
admin index: 6103 cells, 43193 polygons
Done.


**43,193** — gained 1 more polygon from the split-at-shared-nodes approach (was 43,192). Let me check the diff.


ssh node3 "/tmp/diff_admin /tmp/de-parallel-split /tmp/de-sequential-final 2>&1 | head -12"

Compare split approach vs sequential


New code: 43193 polygons
Old code: 43221 polygons
Missing from new: 41
Extra in new: 13

=== Missing by admin level ===
  Level 2: 1 polygons
  Level 8: 4 polygons
  Level 9: 3 polygons
  Level 10: 33 polygons

=== Top 20 missing names ===


41 missing, 13 extra — gained 1 from the split approach. The remaining 41 are genuinely broken relations where the ways have gaps (endpoints 300m+ apart, as we confirmed). These can't be fixed with exact coordinate matching — they need gap-tolerant stitching.

The 41 missing break down as:
- Level 2 (1): international boundary (extends outside extract)
- Level 8 (4): cross-border municipalities
- Level 9 (3): cross-border districts
- Level 10 (33): small areas with genuinely broken ring data

These are cases where osmium's assembler produces polygons that our approach doesn't. Osmium likely handles them through its more sophisticated `find_candidates` logic which can match open ring endpoints that are nearby (not exact).

Let me commit this progress and then add carefully bounded gap closing — only for rings where all ways are present but endpoints don't match.


git add builder/src/build_index.cpp && git commit -m "$(cat <<'EOF'
feat: split ways at shared internal nodes for segment-level ring assembly

Ways are now split at any coordinate that appears in multiple member ways,
creating sub-ways that connect at all shared points (not just endpoints).
This matches osmium's segment-level approach and handles cases where
adjacent admin areas share internal way nodes.

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

Commit split-at-shared-nodes fix


[main e22af43] feat: split ways at shared internal nodes for segment-level ring assembly
 1 file changed, 83 insertions(+), 36 deletions(-)


The 41 remaining failures are genuinely incomplete rings in the Germany extract. Let me verify this is also what happens in the baseline and sequential builds — if osmium also fails on some of these, our gap may be even smaller than it appears. Let me do a proper three-way comparison.


ssh node3 "/tmp/compare_indexes /tmp/de-parallel-split /tmp/de-sequential-final --semantic 2>&1 && echo && /tmp/compare_indexes ~/de-baseline-n3 /tmp/de-parallel-split --semantic 2>&1" 2>&1

Three-way semantic comparison


Error: Exit code 1
Comparing (semantic/cross-version):
  A: /tmp/de-parallel-split
  B: /tmp/de-sequential-final

Loading admin polygons... done.
FAIL: admin_polygons — A=43193 B=43221 matched=43183 only_in_A=10 only_in_B=38
Loading street ways... done.
PASS: street_ways (3312673 items, all matched)
Loading address points... done.
PASS: address_points (19951156 items, all matched)
Loading interpolation ways... done.
PASS: interp_ways (78 items, all matched)
INFO: admin_cells.bin same size (73236 bytes) but different content
PASS: geo_cells.bin same size (195412000 bytes)

=== SOME COMPARISONS FAILED ===


Good data. Semantic comparison of parallel-split vs sequential:
- **Ways: PASS** (3,312,673 all matched)
- **Addresses: PASS** (19,951,156 all matched)
- **Interps: PASS** (78 all matched)
- **Admin: 43,183 matched** — 10 only in parallel, 38 only in sequential

The 10 "only in parallel" are polygons our stitcher creates that osmium doesn't (likely from the split-node coordinate matching). The 38 "only in sequential" are the ones osmium creates but we can't (genuinely incomplete rings that osmium handles with its more sophisticated logic).

Net: 28 fewer (43,193 vs 43,221). That's **99.94% match** on admin polygons, with **100% match** on all other data types.

The remaining 28 are genuinely incomplete boundary relations in the Germany extract where way endpoints are hundreds of meters apart. Osmium handles a few of these through its internal `find_candidates` + `join_connected_rings` logic that can bridge nearby open ring endpoints. To match this exactly, we'd need to implement osmium's full ring-joining heuristic (which is several hundred lines of complex topology code).

Given we're at 100% on ways, addresses, and interps, and 99.94% on admin polygons — shall I pursue the final 28, or is this acceptable to ship?


yes, pursue the last 28 and try mimic what osmium is doing


ssh node3 "wc -l /usr/include/osmium/area/detail/basic_assembler.hpp" 2>&1

Check osmium assembler size


1226 /usr/include/osmium/area/detail/basic_assembler.hpp


ssh node3 "cat /usr/include/osmium/area/detail/basic_assembler.hpp" 2>&1 | head -1200 > /tmp/osmium_assembler.hpp

Copy osmium assembler for analysis


(Bash completed with no output)


   450→                        if (next_segment->start().location() != last_location) {
   451→                            next_segment->reverse();
   452→                        }
   453→                        ring->add_segment_back(next_segment);
   454→                        if (debug()) {
   455→                            std::cerr << "    Next segment is " << *next_segment << "\n";
   456→                        }
   457→                        last_location = next_segment->stop().location();
   458→                    }
   459→
   460→                    ring->fix_direction();
   461→
   462→                    if (debug()) {
   463→                        std::cerr << "    Completed ring: " << *ring << "\n";
   464→                    }
   465→
   466→                    return nodes;
   467→                }
   468→
   469→                uint32_t add_new_ring_complex(const slocation& node) {
   470→                    NodeRefSegment* segment = &m_segment_list[node.item];
   471→                    assert(!segment->is_done());
   472→
   473→                    if (debug()) {
   474→                        std::cerr << "  Starting new ring at location " << node.location(m_segment_list) << " with segment " << *segment << "\n";
   475→                    }
   476→
   477→                    if (node.reverse) {
   478→                        segment->reverse();
   479→                    }
   480→
   481→                    m_rings.emplace_back(segment);
   482→                    ProtoRing* ring = &m_rings.back();
   483→
   484→                    const osmium::Location& first_location = node.location(m_segment_list);
   485→                    osmium::Location last_location = segment->stop().location();
   486→
   487→                    uint32_t nodes = 1;
   488→                    while (first_location != last_location && !is_split_location(last_location)) {
   489→                        ++nodes;
   490→                        NodeRefSegment* next_segment = get_next_segment(last_location);
   491→                        if (next_segment->start().location() != last_location) {
   492→                            next_segment->reverse();
   493→                        }
   494→                        ring->add_segment_back(next_segment);
   495→                        if (debug()) {
   496→                            std::cerr << "    Next segment is " << *next_segment << "\n";
   497→                        }
   498→                        last_location = next_segment->stop().location();
   499→                    }
   500→
   501→                    if (debug()) {
   502→                        if (first_location == last_location) {
   503→                            std::cerr << "    Completed ring: " << *ring << "\n";
   504→                        } else {
   505→                            std::cerr << "    Completed partial ring: " << *ring << "\n";
   506→                        }
   507→                    }
   508→
   509→                    return nodes;
   510→                }
   511→
   512→                void create_locations_list() {
   513→                    m_locations.reserve(m_segment_list.size() * 2);
   514→
   515→                    // static_cast is okay here: The 32bit limit is way past
   516→                    // anything that makes sense here and even if there are
   517→                    // 2^32 segments here, it would simply not go through
   518→                    // all of them not building the multipolygon correctly.
   519→                    assert(m_segment_list.size() < std::numeric_limits<uint32_t>::max());
   520→                    for (uint32_t n = 0; n < static_cast<uint32_t>(m_segment_list.size()); ++n) {
   521→                        m_locations.emplace_back(n, false);
   522→                        m_locations.emplace_back(n, true);
   523→                    }
   524→
   525→                    std::stable_sort(m_locations.begin(), m_locations.end(), [this](const slocation& lhs, const slocation& rhs) {
   526→                        return lhs.location(m_segment_list) < rhs.location(m_segment_list);
   527→                    });
   528→                }
   529→
   530→                void find_inner_outer_complex(ProtoRing* ring) {
   531→                    ProtoRing* outer_ring = find_enclosing_ring(ring->min_segment());
   532→                    if (outer_ring) {
   533→                        outer_ring->add_inner_ring(ring);
   534→                        ring->set_outer_ring(outer_ring);
   535→                    }
   536→                    ring->fix_direction();
   537→                    ring->mark_direction_done();
   538→                }
   539→
   540→                void find_inner_outer_complex() {
   541→                    if (debug()) {
   542→                        std::cerr << "  Finding inner/outer rings\n";
   543→                    }
   544→                    std::vector<ProtoRing*> rings;
   545→                    rings.reserve(m_rings.size());
   546→                    for (auto& ring : m_rings) {
   547→                        if (ring.closed()) {
   548→                            rings.push_back(&ring);
   549→                        }

                        if (next_segment->start().location() != last_location) {
                            next_segment->reverse();
                        }
                        ring->add_segment_back(next_segment);
                        if (debug()) {
                            std::cerr << "    Next segment is " << *next_segment << "\n";
                        }
                        last_location = next_segment->stop().location();
                    }

                    ring->fix_direction();

                    if (debug()) {
                        std::cerr << "    Completed ring: " << *ring << "\n";
                    }

                    return nodes;
                }

                uint32_t add_new_ring_complex(const slocation& node) {
                    NodeRefSegment* segment = &m_segment_list[node.item];
                    assert(!segment->is_done());

                    if (debug()) {
                        std::cerr << "  Starting new ring at location " << node.location(m_segment_list) << " with segment " << *segment << "\n";
                    }

                    if (node.reverse) {
                        segment->reverse();
                    }

                    m_rings.emplace_back(segment);
                    ProtoRing* ring = &m_rings.back();

                    const osmium::Location& first_location = node.location(m_segment_list);
                    osmium::Location last_location = segment->stop().location();

                    uint32_t nodes = 1;
                    while (first_location != last_location && !is_split_location(last_location)) {
                        ++nodes;
                        NodeRefSegment* next_segment = get_next_segment(last_location);
                        if (next_segment->start().location() != last_location) {
                            next_segment->reverse();
                        }
                        ring->add_segment_back(next_segment);
                        if (debug()) {
                            std::cerr << "    Next segment is " << *next_segment << "\n";
                        }
                        last_location = next_segment->stop().location();
                    }

                    if (debug()) {
                        if (first_location == last_location) {
                            std::cerr << "    Completed ring: " << *ring << "\n";
                        } else {
                            std::cerr << "    Completed partial ring: " << *ring << "\n";
                        }
                    }

                    return nodes;
                }

                void create_locations_list() {
                    m_locations.reserve(m_segment_list.size() * 2);

                    // static_cast is okay here: The 32bit limit is way past
                    // anything that makes sense here and even if there are
                    // 2^32 segments here, it would simply not go through
                    // all of them not building the multipolygon correctly.
                    assert(m_segment_list.size() < std::numeric_limits<uint32_t>::max());
                    for (uint32_t n = 0; n < static_cast<uint32_t>(m_segment_list.size()); ++n) {
                        m_locations.emplace_back(n, false);
                        m_locations.emplace_back(n, true);
                    }

                    std::stable_sort(m_locations.begin(), m_locations.end(), [this](const slocation& lhs, const slocation& rhs) {
                        return lhs.location(m_segment_list) < rhs.location(m_segment_list);
                    });
                }

                void find_inner_outer_complex(ProtoRing* ring) {
                    ProtoRing* outer_ring = find_enclosing_ring(ring->min_segment());
                    if (outer_ring) {
                        outer_ring->add_inner_ring(ring);
                        ring->set_outer_ring(outer_ring);
                    }
                    ring->fix_direction();
                    ring->mark_direction_done();
                }

                void find_inner_outer_complex() {
                    if (debug()) {
                        std::cerr << "  Finding inner/outer rings\n";
                    }
                    std::vector<ProtoRing*> rings;
                    rings.reserve(m_rings.size());
                    for (auto& ring : m_rings) {
                        if (ring.closed()) {
                            rings.push_back(&ring);
                        }


   680→                            return false;
   681→                        }
   682→                        auto after = std::next(it, 2);
   683→                        if (after == xrings.cend() || after->location != it->location) {
   684→                            if (debug()) {
   685→                                std::cerr << "      Merging two rings\n";
   686→                            }
   687→                            merge_two_rings(open_ring_its, *it, *std::next(it));
   688→                            return true;
   689→                        }
   690→                        while (it != xrings.cend() && it->location == after->location) {
   691→                            ++it;
   692→                        }
   693→                    }
   694→
   695→                    return false;
   696→                }
   697→
   698→                bool there_are_open_rings() const noexcept {
   699→                    return std::any_of(m_rings.cbegin(), m_rings.cend(), [](const ProtoRing& ring) {
   700→                        return !ring.closed();
   701→                    });
   702→                }
   703→
   704→                struct candidate {
   705→                    int64_t sum;
   706→                    std::vector<std::pair<location_to_ring_map, bool>> rings;
   707→                    osmium::Location start_location;
   708→                    osmium::Location stop_location;
   709→
   710→                    explicit candidate(location_to_ring_map& ring, bool reverse) :
   711→                        sum(ring.ring().sum()),
   712→                        start_location(ring.ring().get_node_ref_start().location()),
   713→                        stop_location(ring.ring().get_node_ref_stop().location()) {
   714→                        rings.emplace_back(ring, reverse);
   715→                    }
   716→
   717→                    bool closed() const noexcept {
   718→                        return start_location == stop_location;
   719→                    }
   720→
   721→                };
   722→
   723→                struct exceeded_max_depth : public std::exception {};
   724→
   725→                using location_set = std::vector<osmium::Location>;
   726→
   727→                void find_candidates(std::vector<candidate>& candidates, location_set& loc_done, const std::vector<location_to_ring_map>& xrings, const candidate& cand, unsigned depth = 0) {
   728→                    if (depth > max_depth) {
   729→                        throw exceeded_max_depth{};
   730→                    }
   731→
   732→                    if (debug()) {
   733→                        std::cerr << "      find_candidates sum=" << cand.sum << " start=" << cand.start_location << " stop=" << cand.stop_location << "\n";
   734→                        for (const auto& ring : cand.rings) {
   735→                            std::cerr << "        " << ring.first.ring() << (ring.second ? " reverse" : "") << "\n";
   736→                        }
   737→                    }
   738→
   739→                    const auto connections = make_range(std::equal_range(xrings.cbegin(),
   740→                                                                         xrings.cend(),
   741→                                                                         location_to_ring_map{cand.stop_location}));
   742→
   743→                    assert(connections.begin() != connections.end());
   744→
   745→                    assert(!cand.rings.empty());
   746→                    const ProtoRing* ring_leading_here = &cand.rings.back().first.ring();
   747→                    for (const location_to_ring_map& m : connections) {
   748→                        const ProtoRing& ring = m.ring();
   749→
   750→                        if (&ring != ring_leading_here) {
   751→                            if (debug()) {
   752→                                std::cerr << "        next possible connection: " << ring << (m.start ? "" : " reverse") << "\n";
   753→                            }
   754→
   755→                            candidate c = cand;
   756→                            if (m.start) {
   757→                                c.rings.emplace_back(m, false);
   758→                                c.stop_location = ring.get_node_ref_stop().location();
   759→                                c.sum += ring.sum();
   760→                            } else {
   761→                                c.rings.emplace_back(m, true);
   762→                                c.stop_location = ring.get_node_ref_start().location();
   763→                                c.sum -= ring.sum();
   764→                            }
   765→                            if (c.closed()) {
   766→                                if (debug()) {
   767→                                    std::cerr << "          found candidate\n";
   768→                                }
   769→
   770→                                if (candidates.empty()) {
   771→                                    candidates.push_back(c);
   772→                                } else if (candidates.size() == 1) {
   773→                                    // add new candidate to vector, keep sorted
   774→                                    if (std::abs(c.sum) < std::abs(candidates.front().sum)) {
   775→                                        candidates.insert(candidates.begin(), c);
   776→                                    } else {
   777→                                        candidates.push_back(c);
   778→                                    }
   779→                                } else {
   780→                                    // add new candidate if it has either smallest or largest area
   781→                                    if (std::abs(c.sum) < std::abs(candidates.front().sum)) {
   782→                                        candidates.front() = c;
   783→                                    } else if (std::abs(c.sum) > std::abs(candidates.back().sum)) {
   784→                                        candidates.back() = c;
   785→                                    }
   786→                                }
   787→                            } else if (std::find(loc_done.cbegin(), loc_done.cend(), c.stop_location) == loc_done.cend()) {
   788→                                if (debug()) {
   789→                                    std::cerr << "          recurse... (depth=" << depth << " candidates.size=" << candidates.size() << " loc_done.size=" << loc_done.size() << ")\n";
   790→                                }
   791→                                loc_done.push_back(c.stop_location);
   792→                                find_candidates(candidates, loc_done, xrings, c, depth + 1);
   793→                                assert(!loc_done.empty() && loc_done.back() == c.stop_location);
   794→                                loc_done.pop_back();
   795→                                if (debug()) {
   796→                                    std::cerr << "          ...back\n";
   797→                                }
   798→                            } else if (debug()) {
   799→                                std::cerr << "          loop found\n";
   800→                            }
   801→                        }
   802→                    }
   803→                }
   804→
   805→                /**
   806→                 * If there are multiple open rings and multiple ways to join them,
   807→                 * this function is called. It will take the first open ring and
   808→                 * try recursively all ways of closing it. Of all the candidates
   809→                 * the one with the smallest area is chosen and closed. If it
   810→                 * can't close this ring, an error is reported and the function
   811→                 * returns false.
   812→                 */
   813→                bool join_connected_rings(open_ring_its_type& open_ring_its) {
   814→                    assert(!open_ring_its.empty());
   815→
   816→                    if (debug()) {
   817→                        std::cerr << "    Trying to merge " << open_ring_its.size() << " open rings (join_connected_rings)\n";
   818→                    }
   819→
   820→                    std::vector<location_to_ring_map> xrings = create_location_to_ring_map(open_ring_its);
   821→
   822→                    const auto ring_min = std::min_element(xrings.begin(), xrings.end(), [](const location_to_ring_map& lhs, const location_to_ring_map& rhs) {
   823→                        return lhs.ring().min_segment() < rhs.ring().min_segment();
   824→                    });
   825→
   826→                    find_inner_outer_complex();
   827→                    ProtoRing* outer_ring = find_enclosing_ring(ring_min->ring().min_segment());
   828→                    const bool ring_min_is_outer = !outer_ring;
   829→                    if (debug()) {
   830→                        std::cerr << "  Open ring is " << (ring_min_is_outer ? "outer" : "inner") << " ring\n";
   831→                    }
   832→                    for (auto& ring : m_rings) {
   833→                        ring.reset();
   834→                    }
   835→
   836→                    const candidate cand{*ring_min, false};
   837→
   838→                    // Locations we have visited while finding candidates, used
   839→                    // to detect loops.
   840→                    location_set loc_done;
   841→
   842→                    loc_done.push_back(cand.stop_location);
   843→
   844→                    std::vector<candidate> candidates;
   845→                    try {
   846→                        find_candidates(candidates, loc_done, xrings, cand);
   847→                    } catch (const exceeded_max_depth&) {
   848→                        if (m_config.debug_level > 0) {
   849→                            std::cerr << "    Exceeded max depth (" << static_cast<unsigned>(max_depth) << ")\n";
   850→                        }
   851→                        return false;
   852→                    }
   853→
   854→                    if (candidates.empty()) {
   855→                        if (debug()) {
   856→                            std::cerr << "    Found no candidates\n";
   857→                        }
   858→                        if (!open_ring_its.empty()) {
   859→                            ++m_stats.open_rings;
   860→                            if (m_config.problem_reporter) {
   861→                                for (auto& it : open_ring_its) {
   862→                                    m_config.problem_reporter->report_ring_not_closed(it->get_node_ref_start(), nullptr);
   863→                                    m_config.problem_reporter->report_ring_not_closed(it->get_node_ref_stop(), nullptr);
   864→                                }
   865→                            }
   866→                        }
   867→                        return false;
   868→                    }
   869→
   870→                    if (debug()) {
   871→                        std::cerr << "    Found candidates:\n";
   872→                        for (const auto& c : candidates) {
   873→                            std::cerr << "      sum=" << c.sum << "\n";
   874→                            for (const auto& ring : c.rings) {
   875→                                std::cerr << "        " << ring.first.ring() << (ring.second ? " reverse" : "") << "\n";
   876→                            }
   877→                        }
   878→                    }
   879→

                            return false;
                        }
                        auto after = std::next(it, 2);
                        if (after == xrings.cend() || after->location != it->location) {
                            if (debug()) {
                                std::cerr << "      Merging two rings\n";
                            }
                            merge_two_rings(open_ring_its, *it, *std::next(it));
                            return true;
                        }
                        while (it != xrings.cend() && it->location == after->location) {
                            ++it;
                        }
                    }

                    return false;
                }

                bool there_are_open_rings() const noexcept {
                    return std::any_of(m_rings.cbegin(), m_rings.cend(), [](const ProtoRing& ring) {
                        return !ring.closed();
                    });
                }

                struct candidate {
                    int64_t sum;
                    std::vector<std::pair<location_to_ring_map, bool>> rings;
                    osmium::Location start_location;
                    osmium::Location stop_location;

                    explicit candidate(location_to_ring_map& ring, bool reverse) :
                        sum(ring.ring().sum()),
                        start_location(ring.ring().get_node_ref_start().location()),
                        stop_location(ring.ring().get_node_ref_stop().location()) {
                        rings.emplace_back(ring, reverse);
                    }

                    bool closed() const noexcept {
                        return start_location == stop_location;
                    }

                };

                struct exceeded_max_depth : public std::exception {};

                using location_set = std::vector<osmium::Location>;

                void find_candidates(std::vector<candidate>& candidates, location_set& loc_done, const std::vector<location_to_ring_map>& xrings, const candidate& cand, unsigned depth = 0) {
                    if (depth > max_depth) {
                        throw exceeded_max_depth{};
                    }

                    if (debug()) {
                        std::cerr << "      find_candidates sum=" << cand.sum << " start=" << cand.start_location << " stop=" << cand.stop_location << "\n";
                        for (const auto& ring : cand.rings) {
                            std::cerr << "        " << ring.first.ring() << (ring.second ? " reverse" : "") << "\n";
                        }
                    }

                    const auto connections = make_range(std::equal_range(xrings.cbegin(),
                                                                         xrings.cend(),
                                                                         location_to_ring_map{cand.stop_location}));

                    assert(connections.begin() != connections.end());

                    assert(!cand.rings.empty());
                    const ProtoRing* ring_leading_here = &cand.rings.back().first.ring();
                    for (const location_to_ring_map& m : connections) {
                        const ProtoRing& ring = m.ring();

                        if (&ring != ring_leading_here) {
                            if (debug()) {
                                std::cerr << "        next possible connection: " << ring << (m.start ? "" : " reverse") << "\n";
                            }

                            candidate c = cand;
                            if (m.start) {
                                c.rings.emplace_back(m, false);
                                c.stop_location = ring.get_node_ref_stop().location();
                                c.sum += ring.sum();
                            } else {
                                c.rings.emplace_back(m, true);
                                c.stop_location = ring.get_node_ref_start().location();
                                c.sum -= ring.sum();
                            }
                            if (c.closed()) {
                                if (debug()) {
                                    std::cerr << "          found candidate\n";
                                }

                                if (candidates.empty()) {
                                    candidates.push_back(c);
                                } else if (candidates.size() == 1) {
                                    // add new candidate to vector, keep sorted
                                    if (std::abs(c.sum) < std::abs(candidates.front().sum)) {
                                        candidates.insert(candidates.begin(), c);
                                    } else {
                                        candidates.push_back(c);
                                    }
                                } else {
                                    // add new candidate if it has either smallest or largest area
                                    if (std::abs(c.sum) < std::abs(candidates.front().sum)) {
                                        candidates.front() = c;
                                    } else if (std::abs(c.sum) > std::abs(candidates.back().sum)) {
                                        candidates.back() = c;
                                    }
                                }
                            } else if (std::find(loc_done.cbegin(), loc_done.cend(), c.stop_location) == loc_done.cend()) {
                                if (debug()) {
                                    std::cerr << "          recurse... (depth=" << depth << " candidates.size=" << candidates.size() << " loc_done.size=" << loc_done.size() << ")\n";
                                }
                                loc_done.push_back(c.stop_location);
                                find_candidates(candidates, loc_done, xrings, c, depth + 1);
                                assert(!loc_done.empty() && loc_done.back() == c.stop_location);
                                loc_done.pop_back();
                                if (debug()) {
                                    std::cerr << "          ...back\n";
                                }
                            } else if (debug()) {
                                std::cerr << "          loop found\n";
                            }
                        }
                    }
                }

                /**
                 * If there are multiple open rings and multiple ways to join them,
                 * this function is called. It will take the first open ring and
                 * try recursively all ways of closing it. Of all the candidates
                 * the one with the smallest area is chosen and closed. If it
                 * can't close this ring, an error is reported and the function
                 * returns false.
                 */
                bool join_connected_rings(open_ring_its_type& open_ring_its) {
                    assert(!open_ring_its.empty());

                    if (debug()) {
                        std::cerr << "    Trying to merge " << open_ring_its.size() << " open rings (join_connected_rings)\n";
                    }

                    std::vector<location_to_ring_map> xrings = create_location_to_ring_map(open_ring_its);

                    const auto ring_min = std::min_element(xrings.begin(), xrings.end(), [](const location_to_ring_map& lhs, const location_to_ring_map& rhs) {
                        return lhs.ring().min_segment() < rhs.ring().min_segment();
                    });

                    find_inner_outer_complex();
                    ProtoRing* outer_ring = find_enclosing_ring(ring_min->ring().min_segment());
                    const bool ring_min_is_outer = !outer_ring;
                    if (debug()) {
                        std::cerr << "  Open ring is " << (ring_min_is_outer ? "outer" : "inner") << " ring\n";
                    }
                    for (auto& ring : m_rings) {
                        ring.reset();
                    }

                    const candidate cand{*ring_min, false};

                    // Locations we have visited while finding candidates, used
                    // to detect loops.
                    location_set loc_done;

                    loc_done.push_back(cand.stop_location);

                    std::vector<candidate> candidates;
                    try {
                        find_candidates(candidates, loc_done, xrings, cand);
                    } catch (const exceeded_max_depth&) {
                        if (m_config.debug_level > 0) {
                            std::cerr << "    Exceeded max depth (" << static_cast<unsigned>(max_depth) << ")\n";
                        }
                        return false;
                    }

                    if (candidates.empty()) {
                        if (debug()) {
                            std::cerr << "    Found no candidates\n";
                        }
                        if (!open_ring_its.empty()) {
                            ++m_stats.open_rings;
                            if (m_config.problem_reporter) {
                                for (auto& it : open_ring_its) {
                                    m_config.problem_reporter->report_ring_not_closed(it->get_node_ref_start(), nullptr);
                                    m_config.problem_reporter->report_ring_not_closed(it->get_node_ref_stop(), nullptr);
                                }
                            }
                        }
                        return false;
                    }

                    if (debug()) {
                        std::cerr << "    Found candidates:\n";
                        for (const auto& c : candidates) {
                            std::cerr << "      sum=" << c.sum << "\n";
                            for (const auto& ring : c.rings) {
                                std::cerr << "        " << ring.first.ring() << (ring.second ? " reverse" : "") << "\n";
                            }
                        }
                    }


   879→
   880→                    // Find the candidate with the smallest/largest area
   881→                    const auto chosen_cand = ring_min_is_outer ? candidates.front() : candidates.back();
   882→
   883→                    if (debug()) {
   884→                        std::cerr << "    Decided on: sum=" << chosen_cand.sum << "\n";
   885→                        for (const auto& ring : chosen_cand.rings) {
   886→                            std::cerr << "        " << ring.first.ring() << (ring.second ? " reverse" : "") << "\n";
   887→                        }
   888→                    }
   889→
   890→                    // Join all (open) rings in the candidate to get one closed ring.
   891→                    assert(chosen_cand.rings.size() > 1);
   892→                    const auto& first_ring = chosen_cand.rings.front().first;
   893→                    const ProtoRing& remaining_ring = first_ring.ring();
   894→                    for (auto it = std::next(chosen_cand.rings.begin()); it != chosen_cand.rings.end(); ++it) {
   895→                        merge_two_rings(open_ring_its, first_ring, it->first);
   896→                    }
   897→
   898→                    if (debug()) {
   899→                        std::cerr << "    Merged to " << remaining_ring << '\n';
   900→                    }
   901→
   902→                    return true;
   903→                }
   904→
   905→                bool create_rings_complex_case() {
   906→                    // First create all the (partial) rings starting at the split locations
   907→                    auto count_remaining = m_segment_list.size();
   908→                    for (const osmium::Location& location : m_split_locations) {
   909→                        const auto locs = make_range(std::equal_range(m_locations.begin(),
   910→                                                                      m_locations.end(),
   911→                                                                      slocation{},
   912→                                                                      [this, &location](const slocation& lhs, const slocation& rhs) {
   913→                                                                          return lhs.location(m_segment_list, location) < rhs.location(m_segment_list, location);
   914→                                                                      }));
   915→                        for (auto& loc : locs) {
   916→                            if (!m_segment_list[loc.item].is_done()) {
   917→                                count_remaining -= add_new_ring_complex(loc);
   918→                                if (count_remaining == 0) {
   919→                                    break;
   920→                                }
   921→                            }
   922→                        }
   923→                    }
   924→
   925→                    // Now find all the rest of the rings (ie not starting at split locations)
   926→                    if (count_remaining > 0) {
   927→                        for (const slocation& sl : m_locations) {
   928→                            const NodeRefSegment& segment = m_segment_list[sl.item];
   929→                            if (!segment.is_done()) {
   930→                                count_remaining -= add_new_ring_complex(sl);
   931→                                if (count_remaining == 0) {
   932→                                    break;
   933→                                }
   934→                            }
   935→                        }
   936→                    }
   937→
   938→                    // Now all segments are in exactly one (partial) ring.
   939→
   940→                    // If there are open rings, try to join them to create closed
   941→                    // rings.
   942→                    if (there_are_open_rings()) {
   943→                        ++m_stats.area_really_complex_case;
   944→
   945→                        open_ring_its_type open_ring_its;
   946→                        for (auto it = m_rings.begin(); it != m_rings.end(); ++it) {
   947→                            if (!it->closed()) {
   948→                                open_ring_its.push_back(it);
   949→                            }
   950→                        }
   951→
   952→                        while (!open_ring_its.empty()) {
   953→                            if (debug()) {
   954→                                std::cerr << "  There are " << open_ring_its.size() << " open rings\n";
   955→                            }
   956→                            while (try_to_merge(open_ring_its)) {
   957→                                // intentionally left blank
   958→                            }
   959→
   960→                            if (!open_ring_its.empty()) {
   961→                                if (debug()) {
   962→                                    std::cerr << "  After joining obvious cases there are still " << open_ring_its.size() << " open rings\n";
   963→                                }
   964→                                if (!join_connected_rings(open_ring_its)) {
   965→                                    return false;
   966→                                }
   967→                            }
   968→                        }
   969→
   970→                        if (debug()) {
   971→                            std::cerr << "  Joined all open rings\n";
   972→                        }
   973→                    }
   974→
   975→                    // Now all rings are complete.
   976→
   977→                    find_inner_outer_complex();
   978→
   979→                    return true;
   980→                }
   981→
   982→#ifdef OSMIUM_WITH_TIMER
   983→                static bool print_header() {
   984→                    std::cout << "nodes outer_rings inner_rings sort dupl intersection locations split simple_case complex_case roles_check\n";
   985→                    return true;
   986→                }
   987→
   988→                static bool init_header() {
   989→                    static bool printed_print_header = print_header();
   990→                    return printed_print_header;
   991→                }
   992→#endif
   993→
   994→            protected:
   995→
   996→                const std::list<ProtoRing>& rings() const noexcept {
   997→                    return m_rings;
   998→                }

                    // Find the candidate with the smallest/largest area
                    const auto chosen_cand = ring_min_is_outer ? candidates.front() : candidates.back();

                    if (debug()) {
                        std::cerr << "    Decided on: sum=" << chosen_cand.sum << "\n";
                        for (const auto& ring : chosen_cand.rings) {
                            std::cerr << "        " << ring.first.ring() << (ring.second ? " reverse" : "") << "\n";
                        }
                    }

                    // Join all (open) rings in the candidate to get one closed ring.
                    assert(chosen_cand.rings.size() > 1);
                    const auto& first_ring = chosen_cand.rings.front().first;
                    const ProtoRing& remaining_ring = first_ring.ring();
                    for (auto it = std::next(chosen_cand.rings.begin()); it != chosen_cand.rings.end(); ++it) {
                        merge_two_rings(open_ring_its, first_ring, it->first);
                    }

                    if (debug()) {
                        std::cerr << "    Merged to " << remaining_ring << '\n';
                    }

                    return true;
                }

                bool create_rings_complex_case() {
                    // First create all the (partial) rings starting at the split locations
                    auto count_remaining = m_segment_list.size();
                    for (const osmium::Location& location : m_split_locations) {
                        const auto locs = make_range(std::equal_range(m_locations.begin(),
                                                                      m_locations.end(),
                                                                      slocation{},
                                                                      [this, &location](const slocation& lhs, const slocation& rhs) {
                                                                          return lhs.location(m_segment_list, location) < rhs.location(m_segment_list, location);
                                                                      }));
                        for (auto& loc : locs) {
                            if (!m_segment_list[loc.item].is_done()) {
                                count_remaining -= add_new_ring_complex(loc);
                                if (count_remaining == 0) {
                                    break;
                                }
                            }
                        }
                    }

                    // Now find all the rest of the rings (ie not starting at split locations)
                    if (count_remaining > 0) {
                        for (const slocation& sl : m_locations) {
                            const NodeRefSegment& segment = m_segment_list[sl.item];
                            if (!segment.is_done()) {
                                count_remaining -= add_new_ring_complex(sl);
                                if (count_remaining == 0) {
                                    break;
                                }
                            }
                        }
                    }

                    // Now all segments are in exactly one (partial) ring.

                    // If there are open rings, try to join them to create closed
                    // rings.
                    if (there_are_open_rings()) {
                        ++m_stats.area_really_complex_case;

                        open_ring_its_type open_ring_its;
                        for (auto it = m_rings.begin(); it != m_rings.end(); ++it) {
                            if (!it->closed()) {
                                open_ring_its.push_back(it);
                            }
                        }

                        while (!open_ring_its.empty()) {
                            if (debug()) {
                                std::cerr << "  There are " << open_ring_its.size() << " open rings\n";
                            }
                            while (try_to_merge(open_ring_its)) {
                                // intentionally left blank
                            }

                            if (!open_ring_its.empty()) {
                                if (debug()) {
                                    std::cerr << "  After joining obvious cases there are still " << open_ring_its.size() << " open rings\n";
                                }
                                if (!join_connected_rings(open_ring_its)) {
                                    return false;
                                }
                            }
                        }

                        if (debug()) {
                            std::cerr << "  Joined all open rings\n";
                        }
                    }

                    // Now all rings are complete.

                    find_inner_outer_complex();

                    return true;
                }

#ifdef OSMIUM_WITH_TIMER
                static bool print_header() {
                    std::cout << "nodes outer_rings inner_rings sort dupl intersection locations split simple_case complex_case roles_check\n";
                    return true;
                }

                static bool init_header() {
                    static bool printed_print_header = print_header();
                    return printed_print_header;
                }
#endif

            protected:

                const std::list<ProtoRing>& rings() const noexcept {
                    return m_rings;
                }


Let me check whether osmium actually produces these "missing" polygons or if the diff tool is miscounting.


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

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

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 + \"/admin_polygons.bin\");
        auto* polys = (AdminPolygon*)data.data();
        size_t count = data.size() / sizeof(AdminPolygon);

        // Search for specific names
        std::set<std::string> targets = {\"55543\", \"55546\", \"Reislingen\", \"Hehlingen\",
            \"Neuhaus-Reislingen\", \"Derichsweiler\", \"Düren\", \"Deutschland - Luxembourg / Luxemburg / Lëtzebuerg\",
            \"France - Deutschland\", \"Großfischbach\", \"Siedelsbrunn\"};

        printf(\"%s (%zu polygons):\\n\", dir.c_str(), count);
        for (size_t i = 0; i < count; i++) {
            const char* name = (polys[i].name_id < strings.size()) ? strings.data() + polys[i].name_id : \"?\";
            if (targets.count(name)) {
                printf(\"  Found: '%s' level=%d verts=%d area=%.2f\\n\",
                       name, polys[i].admin_level, polys[i].vertex_count, polys[i].area);
            }
        }
        printf(\"\\n\");
    }
    return 0;
}
EOFCPP
g++ -O2 -std=c++17 -o /tmp/check_admin /tmp/check_admin.cpp && /tmp/check_admin /tmp/de-parallel-split /tmp/de-sequential-final ~/de-baseline-n3" 2>&1

Check if specific polygons exist in each build


/tmp/de-parallel-split (43193 polygons):
  Found: 'Düren' level=8 verts=335 area=0.01
  Found: 'Siedelsbrunn' level=9 verts=284 area=0.00
  Found: 'Großfischbach' level=10 verts=51 area=0.00
  Found: 'Großfischbach' level=10 verts=15 area=0.00
  Found: 'Düren' level=10 verts=201 area=0.00
  Found: 'Düren' level=9 verts=65 area=0.00

/tmp/de-sequential-final (43221 polygons):
  Found: 'Siedelsbrunn' level=9 verts=284 area=0.00
  Found: 'Düren' level=9 verts=65 area=0.00
  Found: 'Großfischbach' level=10 verts=51 area=0.00
  Found: 'Großfischbach' level=10 verts=15 area=0.00
  Found: 'Großfischbach' level=10 verts=51 area=0.00
  Found: 'Großfischbach' level=10 verts=15 area=0.00
  Found: 'Düren' level=10 verts=201 area=0.00
  Found: 'Düren' level=8 verts=335 area=0.01

/home/michtest/de-baseline-n3 (43221 polygons):
  Found: 'Siedelsbrunn' level=9 verts=284 area=0.00
  Found: 'Düren' level=9 verts=65 area=0.00
  Found: 'Großfischbach' level=10 verts=51 area=0.00
  Found: 'Großfischbach' level=10 verts=15 area=0.00
  Found: 'Großfischbach' level=10 verts=51 area=0.00
  Found: 'Großfischbach' level=10 verts=15 area=0.00
  Found: 'Düren' level=10 verts=201 area=0.00
  Found: 'Düren' level=8 verts=335 area=0.01


Interesting! Look at the differences:

- **Großfischbach**: Sequential/baseline have **4** instances (2x 51-vert, 2x 15-vert), parallel has only **2** (1x 51, 1x 15). The sequential build produces DUPLICATE polygons!
- **55543, 55546, Reislingen, Hehlingen, Neuhaus-Reislingen, Derichsweiler** — NONE exist in any build! The sequential build doesn't create them either.
- **France-Deutschland, Deutschland-Luxembourg** — also not found in any build.

So the "missing" polygons in the diff are mostly **duplicates** in the sequential build, not genuinely different polygons! Osmium's assembler creates duplicate polygons for some relations (probably from different outer rings), and our stitcher creates the correct single polygon.

Let me verify this by counting duplicates in the sequential build.


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

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

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 + \"/admin_polygons.bin\");
        auto* polys = (AdminPolygon*)data.data();
        size_t count = data.size() / sizeof(AdminPolygon);

        // Count by name|level|vertex_count
        std::map<std::string, int> combos;
        for (size_t i = 0; i < count; i++) {
            const char* name = (polys[i].name_id < strings.size()) ? strings.data() + polys[i].name_id : \"?\";
            std::string key = std::string(name) + \"|\" + std::to_string(polys[i].admin_level) + \"|\" + std::to_string(polys[i].vertex_count);
            combos[key]++;
        }

        int dupes = 0, dupe_combos = 0;
        for (auto& [k, c] : combos) {
            if (c > 1) {
                dupes += (c - 1);
                dupe_combos++;
                if (dupe_combos <= 10) {
                    printf(\"  Duplicate: '%s' x %d\\n\", k.c_str(), c);
                }
            }
        }
        printf(\"%s: %zu polygons, %d duplicates in %d combos\\n\\n\", dir.c_str(), count, dupes, dupe_combos);
    }
    return 0;
}
EOFCPP
g++ -O2 -std=c++17 -o /tmp/count_dupes /tmp/count_dupes.cpp && /tmp/count_dupes /tmp/de-parallel-split /tmp/de-sequential-final ~/de-baseline-n3" 2>&1

Count duplicate admin polygons in each build


  Duplicate: '06237|11|15' x 2
  Duplicate: '06317|11|12' x 2
  Duplicate: '06772|11|380' x 2
  Duplicate: '17322|11|254' x 2
  Duplicate: '17375|11|499' x 2
  Duplicate: '17440|11|180' x 2
  Duplicate: '17440|11|499' x 2
  Duplicate: '17493|11|118' x 2
  Duplicate: '18249|11|499' x 2
  Duplicate: '18374|11|499' x 2
/tmp/de-parallel-split: 43193 polygons, 269 duplicates in 214 combos

  Duplicate: '06237|11|15' x 2
  Duplicate: '06317|11|12' x 2
  Duplicate: '06772|11|380' x 2
  Duplicate: '17322|11|254' x 2
  Duplicate: '17375|11|499' x 2
  Duplicate: '17440|11|180' x 2
  Duplicate: '17440|11|499' x 2
  Duplicate: '17493|11|118' x 2
  Duplicate: '18249|11|499' x 2
  Duplicate: '18374|11|499' x 2
/tmp/de-sequential-final: 43221 polygons, 276 duplicates in 221 combos

  Duplicate: '06237|11|15' x 2
  Duplicate: '06317|11|12' x 2
  Duplicate: '06772|11|380' x 2
  Duplicate: '14778|11|500' x 2
  Duplicate: '17322|11|254' x 2
  Duplicate: '17440|11|180' x 2
  Duplicate: '17440|11|499' x 2
  Duplicate: '17493|11|118' x 2
  Duplicate: '23999|11|11' x 2
  Duplicate: '39114|11|5' x 3
/home/michtest/de-baseline-n3: 43221 polygons, 271 duplicates in 216 combos


The gap is largely **duplicates**! Sequential has 276 duplicates vs our 269. Let me compare deduplicated polygon sets.


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

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

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_unique = [](const std::string& dir) {
        auto strings = read_file(dir + \"/strings.bin\");
        auto data = read_file(dir + \"/admin_polygons.bin\");
        auto* polys = (AdminPolygon*)data.data();
        size_t count = data.size() / sizeof(AdminPolygon);

        // Unique name|level pairs (existence, not count)
        std::set<std::string> unique;
        for (size_t i = 0; i < count; i++) {
            const char* name = (polys[i].name_id < strings.size()) ? strings.data() + polys[i].name_id : \"?\";
            unique.insert(std::string(name) + \"|\" + std::to_string(polys[i].admin_level));
        }
        return unique;
    };

    auto a = load_unique(argv[1]);
    auto b = load_unique(argv[2]);

    printf(\"A (%s): %zu unique name|level\\n\", argv[1], a.size());
    printf(\"B (%s): %zu unique name|level\\n\", argv[2], b.size());

    int only_a = 0, only_b = 0, both = 0;
    std::vector<std::string> missing_from_a, missing_from_b;
    for (auto& k : a) { if (b.count(k)) both++; else { only_a++; missing_from_b.push_back(k); } }
    for (auto& k : b) { if (!a.count(k)) { only_b++; missing_from_a.push_back(k); } }

    printf(\"Both: %d  Only_A: %d  Only_B: %d\\n\", both, only_a, only_b);

    if (!missing_from_b.empty()) {
        printf(\"\\nOnly in A (not in B):\\n\");
        for (int i = 0; i < 15 && i < (int)missing_from_b.size(); i++)
            printf(\"  %s\\n\", missing_from_b[i].c_str());
    }
    if (!missing_from_a.empty()) {
        printf(\"\\nOnly in B (not in A):\\n\");
        for (int i = 0; i < 15 && i < (int)missing_from_a.size(); i++)
            printf(\"  %s\\n\", missing_from_a[i].c_str());
    }
    return 0;
}
EOFCPP
g++ -O2 -std=c++17 -o /tmp/dedup_compare /tmp/dedup_compare.cpp && echo '=== Parallel vs Sequential ===' && /tmp/dedup_compare /tmp/de-parallel-split /tmp/de-sequential-final && echo && echo '=== Parallel vs Baseline ===' && /tmp/dedup_compare /tmp/de-parallel-split ~/de-baseline-n3" 2>&1

Compare deduplicated admin polygon sets


=== Parallel vs Sequential ===
A (/tmp/de-parallel-split): 37013 unique name|level
B (/tmp/de-sequential-final): 37034 unique name|level
Both: 37005  Only_A: 8  Only_B: 29

Only in A (not in B):
  Bezirk Reutte|6
  Deutschland - Österreich|2
  Deutschland — Schweiz / Suisse / Svizerra|2
  Glattfelden|8
  Lobith|10
  Tirol|4
  Zurzach|8
  Österreich|2

Only in B (not in A):
  Alpe|10
  Attendorn|10
  Brächen|10
  Böbber|10
  Büddelhagen|10
  Deutmecke|10
  Deutschland - België / Belgique / Belgien|2
  Eckhalden|10
  Gstütt|10
  Immen|10
  Jennecken|10
  Kleinfischbach|10
  Monsau|10
  Morkepütz|10
  Niederbellinghausen|10

=== Parallel vs Baseline ===
A (/tmp/de-parallel-split): 37013 unique name|level
B (/home/michtest/de-baseline-n3): 37034 unique name|level
Both: 37005  Only_A: 8  Only_B: 29

Only in A (not in B):
  Bezirk Reutte|6
  Deutschland - Österreich|2
  Deutschland — Schweiz / Suisse / Svizerra|2
  Glattfelden|8
  Lobith|10
  Tirol|4
  Zurzach|8
  Österreich|2

Only in B (not in A):
  Alpe|10
  Attendorn|10
  Brächen|10
  Böbber|10
  Büddelhagen|10
  Deutmecke|10
  Deutschland - België / Belgique / Belgien|2
  Eckhalden|10
  Gstütt|10
  Immen|10
  Jennecken|10
  Kleinfischbach|10
  Monsau|10
  Morkepütz|10
  Niederbellinghausen|10


Now we have the exact picture:

- **37,005 unique name|level pairs matched** — present in both builds
- **8 only in parallel** — our stitcher creates these (cross-border: Austria, Switzerland)
- **29 only in sequential** — osmium creates these but we don't

The 8 we create extra are cross-border admin boundaries (Österreich, Tirol, etc.) that our stitcher assembles from partial data. The 29 we're missing are mostly level 10 German municipal subdivisions (Alpe, Attendorn, Brächen, etc.) that osmium somehow builds.

Let me look at one of these level-10 names in detail to understand what osmium does differently.


ssh node3 "cat > /tmp/find_relation.cpp << 'EOFCPP'
#include <cstdint>
#include <cstring>
#include <iostream>
#include <set>
#include <osmium/io/pbf_input.hpp>
#include <osmium/osm/relation.hpp>
int main(int argc, char* argv[]) {
    std::set<std::string> targets = {\"Alpe\", \"Attendorn\", \"Monsau\", \"Kleinfischbach\"};
    osmium::io::Reader reader{argv[1], osmium::osm_entity_bits::relation};
    while (auto buffer = reader.read()) {
        for (const auto& item : buffer) {
            if (item.type() != osmium::item_type::relation) continue;
            const auto& rel = static_cast<const osmium::Relation&>(item);
            const char* name = rel.tags()[\"name\"];
            const char* boundary = rel.tags()[\"boundary\"];
            if (!name || !boundary) continue;
            if (std::strcmp(boundary, \"administrative\") != 0) continue;
            if (!targets.count(name)) continue;
            printf(\"Relation %ld: name='%s' level=%s members=%d\\n\",
                   (long)rel.id(), name, rel.tags()[\"admin_level\"], (int)rel.members().size());
            for (const auto& m : rel.members()) {
                if (m.type() == osmium::item_type::way) {
                    printf(\"  way %ld role='%s'\\n\", (long)m.ref(), m.role());
                }
            }
        }
    }
    return 0;
}
EOFCPP
g++ -O2 -std=c++17 -o /tmp/find_relation /tmp/find_relation.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 && /tmp/find_relation ~/pbf/germany.osm.pbf" 2>&1

Find the actual relations for missing polygons


Relation 163178: name='Attendorn' level=8 members=9
  way 36902722 role='outer'
  way 29413219 role='outer'
  way 37413348 role='outer'
  way 37412828 role='outer'
  way 36902723 role='outer'
  way 36902724 role='outer'
  way 94955988 role='outer'
  way 36902725 role='outer'
  way 36902728 role='outer'


ssh node3 "cat > /tmp/find_rel2.cpp << 'EOFCPP'
#include <cstdint>
#include <cstring>
#include <iostream>
#include <set>
#include <osmium/io/pbf_input.hpp>
#include <osmium/osm/relation.hpp>
int main(int argc, char* argv[]) {
    std::set<std::string> targets = {\"Alpe\", \"Monsau\", \"Kleinfischbach\", \"Büddelhagen\", \"Deutmecke\"};
    osmium::io::Reader reader{argv[1], osmium::osm_entity_bits::relation};
    while (auto buffer = reader.read()) {
        for (const auto& item : buffer) {
            if (item.type() != osmium::item_type::relation) continue;
            const auto& rel = static_cast<const osmium::Relation&>(item);
            const char* name = rel.tags()[\"name\"];
            if (!name || !targets.count(name)) continue;
            const char* level = rel.tags()[\"admin_level\"];
            printf(\"Relation %ld: name='%s' boundary='%s' level=%s members=%d\\n\",
                   (long)rel.id(), name,
                   rel.tags()[\"boundary\"] ? rel.tags()[\"boundary\"] : \"(none)\",
                   level ? level : \"(none)\",
                   (int)rel.members().size());
            int ways = 0;
            for (const auto& m : rel.members()) {
                if (m.type() == osmium::item_type::way) {
                    ways++;
                    if (ways <= 5) printf(\"  way %ld role='%s'\\n\", (long)m.ref(), m.role());
                }
            }
            if (ways > 5) printf(\"  ... and %d more ways\\n\", ways - 5);
        }
    }
    return 0;
}
EOFCPP
g++ -O2 -std=c++17 -o /tmp/find_rel2 /tmp/find_rel2.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 && /tmp/find_rel2 ~/pbf/germany.osm.pbf" 2>&1

Find relations for specific missing level-10 polygons


Relation 2722389: name='Deutmecke' boundary='(none)' level=(none) members=7
  way 202866633 role='main_stream'
  way 202866636 role='main_stream'
  way 202866639 role='main_stream'
  way 202866635 role='main_stream'
  way 202866634 role='main_stream'
  ... and 2 more ways
Relation 3128225: name='Alpe' boundary='(none)' level=(none) members=16
  way 136752360 role='main_stream'
  way 261139425 role='main_stream'
  way 43890225 role='main_stream'
  way 43838547 role='main_stream'
  way 232952961 role='main_stream'
  ... and 11 more ways
Relation 10768615: name='Alpe' boundary='(none)' level=(none) members=3
  way 777403137 role='platform'
Relation 11138440: name='Kleinfischbach' boundary='(none)' level=(none) members=2
Relation 15661120: name='Alpe' boundary='(none)' level=(none) members=9
  way 1158302435 role='main_stream'
  way 262670265 role='main_stream'
  way 262670267 role='main_stream'
  way 262670266 role='main_stream'
  way 262670264 role='main_stream'
  ... and 4 more ways


These aren't even admin relations — they have no `boundary` tag. The sequential build must be getting these from **closed ways** (single ways forming a polygon), not multipolygon relations. Osmium's assembler handles both relations AND closed ways. Our parallel assembler only handles relations.

Let me verify this.


ssh node3 "cat > /tmp/find_admin_ways.cpp << 'EOFCPP'
#include <cstdint>
#include <cstring>
#include <iostream>
#include <set>
#include <osmium/io/pbf_input.hpp>
#include <osmium/osm/way.hpp>
int main(int argc, char* argv[]) {
    std::set<std::string> targets = {\"Alpe\", \"Monsau\", \"Kleinfischbach\", \"Büddelhagen\",
        \"Deutmecke\", \"Brächen\", \"Eckhalden\", \"Gstütt\", \"Immen\", \"Jennecken\"};
    int found = 0;
    osmium::io::Reader reader{argv[1], osmium::osm_entity_bits::way};
    while (auto buffer = reader.read()) {
        for (const auto& item : buffer) {
            if (item.type() != osmium::item_type::way) continue;
            const auto& way = static_cast<const osmium::Way&>(item);
            const char* name = way.tags()[\"name\"];
            if (!name) continue;
            const char* boundary = way.tags()[\"boundary\"];
            const char* admin_level = way.tags()[\"admin_level\"];
            if (!boundary || std::strcmp(boundary, \"administrative\") != 0) continue;
            if (targets.count(name)) {
                auto& nodes = way.nodes();
                bool closed = (nodes.size() >= 4 && nodes.front().ref() == nodes.back().ref());
                printf(\"Way %ld: name='%s' level=%s nodes=%d closed=%s\\n\",
                       (long)way.id(), name, admin_level ? admin_level : \"?\",
                       (int)nodes.size(), closed ? \"yes\" : \"no\");
                found++;
            }
        }
    }
    printf(\"Found %d admin boundary ways\\n\", found);
    return 0;
}
EOFCPP
g++ -O2 -std=c++17 -o /tmp/find_admin_ways /tmp/find_admin_ways.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 && /tmp/find_admin_ways ~/pbf/germany.osm.pbf" 2>&1

Find closed ways with admin boundary tags for missing polygons


Way 143221072: name='Eckhalden' level=10 nodes=5 closed=yes
Way 369069495: name='Gstütt' level=10 nodes=15 closed=yes
Way 390581881: name='Büddelhagen' level=10 nodes=42 closed=yes
Way 396505006: name='Jennecken' level=10 nodes=45 closed=yes
Way 396505009: name='Immen' level=10 nodes=64 closed=yes
Way 396742556: name='Brächen' level=10 nodes=86 closed=yes
Way 398137101: name='Kleinfischbach' level=10 nodes=50 closed=yes
Way 402334192: name='Monsau' level=10 nodes=18 closed=yes
Way 402664563: name='Alpe' level=10 nodes=86 closed=yes
Way 551843503: name='Deutmecke' level=10 nodes=16 closed=yes
Found 10 admin boundary ways


**That's it!** These are all **closed ways** (not multipolygon relations) tagged with `boundary=administrative` + `admin_level=10`. Osmium's assembler processes these as simple way-based polygons. Our parallel assembler only handles multipolygon relations.

The fix: during parallel way processing, detect closed ways with admin boundary tags and add them as admin polygons directly.


No matches found


No handling for closed-way admin boundaries in the parallel code. Let me add it. The parallel way processing already iterates through all ways — I just need to add a check for closed ways with admin boundary tags.


  1978→                    }
  1979→                    reader_nodes.close();
  1980→                    {
  1981→                        std::lock_guard<std::mutex> lock(queue_mutex);
  1982→                        reader_done = true;
  1983→                    }
  1984→                    queue_cv.notify_all();
  1985→                }
  1986→
  1987→                for (auto& w : node_workers) w.join();
  1988→
  1989→                // Merge address points
  1990→                uint64_t total_addrs = 0;
  1991→                for (auto& local : ntld) {
  1992→                    for (size_t j = 0; j < local.addr_coords.size(); j++) {
  1993→                        uint64_t dummy = 0;
  1994→                        add_addr_point(data, local.addr_coords[j].first, local.addr_coords[j].second,
  1995→                                       local.addr_strings[j].first.c_str(),
  1996→                                       local.addr_strings[j].second.c_str(), dummy);
  1997→                    }
  1998→                    total_addrs += local.count;
  1999→                }
  2000→                std::cerr << "  Node processing complete: " << total_addrs
  2001→                          << " address points collected." << std::endl;
  2002→            }

                    }
                    reader_nodes.close();
                    {
                        std::lock_guard<std::mutex> lock(queue_mutex);
                        reader_done = true;
                    }
                    queue_cv.notify_all();
                }

                for (auto& w : node_workers) w.join();

                // Merge address points
                uint64_t total_addrs = 0;
                for (auto& local : ntld) {
                    for (size_t j = 0; j < local.addr_coords.size(); j++) {
                        uint64_t dummy = 0;
                        add_addr_point(data, local.addr_coords[j].first, local.addr_coords[j].second,
                                       local.addr_strings[j].first.c_str(),
                                       local.addr_strings[j].second.c_str(), dummy);
                    }
                    total_addrs += local.count;
                }
                std::cerr << "  Node processing complete: " << total_addrs
                          << " address points collected." << std::endl;
            }


Let me find the ThreadLocalData struct for way processing to add admin polygon storage.


  2022→
  2023→                // Thread-local data for parallel way processing
  2024→                struct ThreadLocalData {
  2025→                    std::vector<WayHeader> ways;
  2026→                    std::vector<NodeCoord> street_nodes;
  2027→                    std::vector<DeferredWay> deferred_ways;
  2028→                    std::vector<InterpWay> interp_ways;
  2029→                    std::vector<NodeCoord> interp_nodes;
  2030→                    std::vector<DeferredInterp> deferred_interps;
  2031→                    std::vector<AddrPoint> building_addrs;
  2032→                    std::vector<std::pair<double, double>> building_addr_coords; // lat,lng for S2 cell
  2033→                    std::vector<std::string> way_strings;      // way name strings
  2034→                    std::vector<std::pair<std::string,std::string>> addr_strings; // building addr {hn, street}
  2035→                    std::vector<std::string> interp_strings;   // interp street name strings
  2036→                    uint64_t way_count = 0;
  2037→                    uint64_t building_addr_count = 0;
  2038→                    uint64_t interp_count = 0;
  2039→                    // Way geometries for parallel admin assembly
  2040→                    struct WayGeomEntry {
  2041→                        int64_t way_id;
  2042→                        std::vector<std::pair<double,double>> coords;
  2043→                        int64_t first_node_id;
  2044→                        int64_t last_node_id;
  2045→                    };
  2046→                    std::vector<WayGeomEntry> way_geoms;
  2047→                };
  2048→
  2049→                // Read buffers and dispatch to thread pool
  2050→                // Streaming producer-consumer for way blocks
  2051→                std::mutex way_queue_mutex;

                // Thread-local data for parallel way processing
                struct ThreadLocalData {
                    std::vector<WayHeader> ways;
                    std::vector<NodeCoord> street_nodes;
                    std::vector<DeferredWay> deferred_ways;
                    std::vector<InterpWay> interp_ways;
                    std::vector<NodeCoord> interp_nodes;
                    std::vector<DeferredInterp> deferred_interps;
                    std::vector<AddrPoint> building_addrs;
                    std::vector<std::pair<double, double>> building_addr_coords; // lat,lng for S2 cell
                    std::vector<std::string> way_strings;      // way name strings
                    std::vector<std::pair<std::string,std::string>> addr_strings; // building addr {hn, street}
                    std::vector<std::string> interp_strings;   // interp street name strings
                    uint64_t way_count = 0;
                    uint64_t building_addr_count = 0;
                    uint64_t interp_count = 0;
                    // Way geometries for parallel admin assembly
                    struct WayGeomEntry {
                        int64_t way_id;
                        std::vector<std::pair<double,double>> coords;
                        int64_t first_node_id;
                        int64_t last_node_id;
                    };
                    std::vector<WayGeomEntry> way_geoms;
                };

                // Read buffers and dispatch to thread pool
                // Streaming producer-consumer for way blocks
                std::mutex way_queue_mutex;


Edit

                    std::vector<WayGeomEntry> way_geoms;
                };

                    std::vector<WayGeomEntry> way_geoms;
                    // Closed-way admin polygons (ways with boundary=administrative)
                    struct ClosedWayAdmin {
                        std::vector<std::pair<double,double>> vertices;
                        std::string name;
                        uint8_t admin_level;
                        std::string country_code;
                    };
                    std::vector<ClosedWayAdmin> closed_way_admins;
                };