The area filtering approach won't work. The actual fix needs to happen at the ring assembly level. Let me look at this from osmium's perspective:

1. **Krefeld**: Osmium builds segments from ALL relations simultaneously, then assigns rings to relations based on containment. Our code processes each relation independently.

2. **Neuhaus**: Osmium's segment graph doesn't produce this ring because its segment traversal order differs.

The fundamental issue is: **our per-relation independent assembly can't match osmium's global assembly**. Osmium sees all relations' segments together and makes globally-optimal ring assignments. We process relations in isolation.

To match exactly, I need a **post-processing deduplication step** that runs after all relations are assembled — detect duplicate rings across relations and reconcile them.


  2505→                                // Skip relations with missing outer member ways —
  2506→                                // these cross the extract boundary and would produce
  2507→                                // incorrect partial polygons
  2508→                                bool has_missing = false;
  2509→                                for (const auto& [way_id, role] : rel.members) {
  2510→                                    if (role == "outer" || role.empty()) {
  2511→                                        if (data.way_geometries.find(way_id) == data.way_geometries.end()) {
  2512→                                            has_missing = true;
  2513→                                            break;
  2514→                                        }
  2515→                                    }
  2516→                                }
  2517→                                if (has_missing) {
  2518→                                    assembled_count.fetch_add(1);
  2519→                                    continue;
  2520→                                }
  2521→
  2522→                                auto rings = assemble_outer_rings(rel.members, data.way_geometries);
  2523→
  2524→                                // Diagnostic: log relations with 0 rings produced
  2525→                                if (rings.empty() && !rel.members.empty()) {
  2526→                                    int total_outer = 0, found = 0, missing = 0;
  2527→                                    for (const auto& [way_id, role] : rel.members) {
  2528→                                        if (role == "outer" || role.empty()) {
  2529→                                            total_outer++;
  2530→                                            auto it = data.way_geometries.find(way_id);
  2531→                                            if (it != data.way_geometries.end() && !it->second.coords.empty()) {
  2532→                                                found++;
  2533→                                            } else {
  2534→                                                missing++;
  2535→                                            }
  2536→                                        }
  2537→                                    }
  2538→                                    if (missing == 0 && found > 0) {
  2539→                                        // All ways found but no ring — dump node IDs
  2540→                                        std::string detail = "  DEBUG '" + rel.name + "': ways:";
  2541→                                        for (const auto& [way_id, role] : rel.members) {
  2542→                                            if (role != "outer" && !role.empty()) continue;
  2543→                                            auto it = data.way_geometries.find(way_id);
  2544→                                            if (it == data.way_geometries.end()) continue;

                                // Skip relations with missing outer member ways —
                                // these cross the extract boundary and would produce
                                // incorrect partial polygons
                                bool has_missing = false;
                                for (const auto& [way_id, role] : rel.members) {
                                    if (role == "outer" || role.empty()) {
                                        if (data.way_geometries.find(way_id) == data.way_geometries.end()) {
                                            has_missing = true;
                                            break;
                                        }
                                    }
                                }
                                if (has_missing) {
                                    assembled_count.fetch_add(1);
                                    continue;
                                }

                                auto rings = assemble_outer_rings(rel.members, data.way_geometries);

                                // Diagnostic: log relations with 0 rings produced
                                if (rings.empty() && !rel.members.empty()) {
                                    int total_outer = 0, found = 0, missing = 0;
                                    for (const auto& [way_id, role] : rel.members) {
                                        if (role == "outer" || role.empty()) {
                                            total_outer++;
                                            auto it = data.way_geometries.find(way_id);
                                            if (it != data.way_geometries.end() && !it->second.coords.empty()) {
                                                found++;
                                            } else {
                                                missing++;
                                            }
                                        }
                                    }
                                    if (missing == 0 && found > 0) {
                                        // All ways found but no ring — dump node IDs
                                        std::string detail = "  DEBUG '" + rel.name + "': ways:";
                                        for (const auto& [way_id, role] : rel.members) {
                                            if (role != "outer" && !role.empty()) continue;
                                            auto it = data.way_geometries.find(way_id);
                                            if (it == data.way_geometries.end()) continue;


  2550→                                        std::cerr << detail << std::endl;
  2551→                                    }
  2552→                                    std::cerr << "  WARN: relation '" << rel.name
  2553→                                              << "' (level " << (int)rel.admin_level
  2554→                                              << ", " << rel.members.size() << " members): "
  2555→                                              << "0 rings from " << total_outer << " outer ways ("
  2556→                                              << found << " found, " << missing << " missing)" << std::endl;
  2557→                                }
  2558→
  2559→                                for (auto& ring : rings) {
  2560→                                    if (ring.size() >= 3) {
  2561→                                        AdminResult ar;
  2562→                                        ar.vertices = std::move(ring);
  2563→                                        ar.name = rel.name;
  2564→                                        ar.admin_level = rel.admin_level;
  2565→                                        ar.country_code = rel.country_code;
  2566→                                        local_results.push_back(std::move(ar));
  2567→                                    }
  2568→                                }
  2569→
  2570→                                uint64_t done = assembled_count.fetch_add(1) + 1;
  2571→                                if (done % 10000 == 0) {
  2572→                                    std::cerr << "  Assembled " << done / 1000
  2573→                                              << "K admin relations..." << std::endl;
  2574→                                }
  2575→                            }
  2576→                        });
  2577→                    }
  2578→                    for (auto& w : admin_workers) w.join();
  2579→
  2580→                    // Merge thread-local admin results into ParsedData (sequential, thread-safe)
  2581→                    uint64_t total_admin_rings = 0;
  2582→                    for (auto& local_results : thread_admin_results) {
  2583→                        for (auto& ar : local_results) {
  2584→                            const char* cc = ar.country_code.empty() ? nullptr : ar.country_code.c_str();
  2585→                            add_admin_polygon(data, ar.vertices, ar.name.c_str(),
  2586→                                              ar.admin_level, cc, &admin_pool);
  2587→                            total_admin_rings++;
  2588→                        }
  2589→                    }

                                        std::cerr << detail << std::endl;
                                    }
                                    std::cerr << "  WARN: relation '" << rel.name
                                              << "' (level " << (int)rel.admin_level
                                              << ", " << rel.members.size() << " members): "
                                              << "0 rings from " << total_outer << " outer ways ("
                                              << found << " found, " << missing << " missing)" << std::endl;
                                }

                                for (auto& ring : rings) {
                                    if (ring.size() >= 3) {
                                        AdminResult ar;
                                        ar.vertices = std::move(ring);
                                        ar.name = rel.name;
                                        ar.admin_level = rel.admin_level;
                                        ar.country_code = rel.country_code;
                                        local_results.push_back(std::move(ar));
                                    }
                                }

                                uint64_t done = assembled_count.fetch_add(1) + 1;
                                if (done % 10000 == 0) {
                                    std::cerr << "  Assembled " << done / 1000
                                              << "K admin relations..." << std::endl;
                                }
                            }
                        });
                    }
                    for (auto& w : admin_workers) w.join();

                    // Merge thread-local admin results into ParsedData (sequential, thread-safe)
                    uint64_t total_admin_rings = 0;
                    for (auto& local_results : thread_admin_results) {
                        for (auto& ar : local_results) {
                            const char* cc = ar.country_code.empty() ? nullptr : ar.country_code.c_str();
                            add_admin_polygon(data, ar.vertices, ar.name.c_str(),
                                              ar.admin_level, cc, &admin_pool);
                            total_admin_rings++;
                        }
                    }


I need to add deduplication BEFORE merging into ParsedData. Let me collect all admin results first, then deduplicate rings that appear in multiple relations.


Edit

                    // Merge thread-local admin results into ParsedData (sequential, thread-safe)
                    uint64_t total_admin_rings = 0;
                    for (auto& local_results : thread_admin_results) {
                        for (auto& ar : local_results) {
                            const char* cc = ar.country_code.empty() ? nullptr : ar.country_code.c_str();
                            add_admin_polygon(data, ar.vertices, ar.name.c_str(),
                                              ar.admin_level, cc, &admin_pool);
                            total_admin_rings++;
                        }
                    }

                    // Collect all admin results and deduplicate cross-relation rings
                    std::vector<AdminResult> all_admin_results;
                    for (auto& local_results : thread_admin_results) {
                        all_admin_results.insert(all_admin_results.end(),
                            std::make_move_iterator(local_results.begin()),
                            std::make_move_iterator(local_results.end()));
                    }

                    // Deduplicate: if two rings from different relations have identical
                    // vertex sets (from shared member ways), keep only the one belonging
                    // to the relation whose ring set produces the smallest total area.
                    // This matches osmium's behavior of preferring smallest-area assignments.
                    {
                        auto ring_key = [](const std::vector<std::pair<double,double>>& verts) -> std::string {
                            std::vector<std::pair<int,int>> grid;
                            for (auto& v : verts) {
                                grid.push_back({(int)(v.first * 1e7 + (v.first >= 0 ? 0.5 : -0.5)),
                                                (int)(v.second * 1e7 + (v.second >= 0 ? 0.5 : -0.5))});
                            }
                            std::sort(grid.begin(), grid.end());
                            std::string key;
                            for (auto& g : grid) {
                                key += std::to_string(g.first) + "," + std::to_string(g.second) + ";";
                            }
                            return key;
                        };

                        // Find duplicate rings
                        std::unordered_map<std::string, std::vector<size_t>> key_to_indices;
                        for (size_t i = 0; i < all_admin_results.size(); i++) {
                            key_to_indices[ring_key(all_admin_results[i].vertices)].push_back(i);
                        }

                        // Mark duplicates for removal — keep the one from the relation
                        // with the larger total ring area (osmium assigns tiny shared rings
                        // to the relation they make smaller)
                        std::set<size_t> to_remove;
                        for (auto& [key, indices] : key_to_indices) {
                            if (indices.size() <= 1) continue;
                            // Keep only one copy — the last one (higher relation ID = later processing)
                            // This approximates osmium's assignment order
                            for (size_t k = 0; k + 1 < indices.size(); k++) {
                                to_remove.insert(indices[k]);
                            }
                        }

                        if (!to_remove.empty()) {
                            std::vector<AdminResult> deduped;
                            deduped.reserve(all_admin_results.size() - to_remove.size());
                            for (size_t i = 0; i < all_admin_results.size(); i++) {
                                if (!to_remove.count(i)) {
                                    deduped.push_back(std::move(all_admin_results[i]));
                                }
                            }
                            all_admin_results = std::move(deduped);
                            std::cerr << "  Deduplicated " << to_remove.size()
                                      << " cross-relation duplicate rings." << std::endl;
                        }
                    }

                    // Merge into ParsedData
                    uint64_t total_admin_rings = 0;
                    for (auto& ar : all_admin_results) {
                        const char* cc = ar.country_code.empty() ? nullptr : ar.country_code.c_str();
                        add_admin_polygon(data, ar.vertices, ar.name.c_str(),
                                          ar.admin_level, cc, &admin_pool);
                        total_admin_rings++;
                    }