Now for the Krefeld and Neuhaus issues. Krefeld-Ost/Uerdingen is a ring assignment difference (net 0). Neuhaus is +1 extra. Let me check if Neuhaus is also a border-line or a closed-way duplicate.


Bash

ssh node3 "/tmp/check_extra /tmp/de-par-final5 /tmp/de-sequential-final 2>/dev/null" 2>&1 | grep -i neuhaus

Compare Neuhaus between builds


  'Neuhaus' level=10 verts=9 area=0.0001
  'Neuhaus' level=10 verts=416 area=0.0023
  'Neuhaus' level=10 verts=402 area=0.0004
  'Neuhaus' level=10 verts=17 area=0.0000
  'Neuhaus' level=10 verts=12 area=0.0000
  'Neuhaus' level=8 verts=432 area=0.0013
  'Neuhaus' level=10 verts=9 area=0.0001
  'Neuhaus' level=10 verts=416 area=0.0023
  'Neuhaus' level=10 verts=402 area=0.0004
  'Neuhaus' level=10 verts=17 area=0.0000
  'Neuhaus' level=8 verts=432 area=0.0013


Parallel has 5 Neuhaus|10 (9, 416, 402, 17, 12 verts) + 1 Neuhaus|8.
Sequential has 4 Neuhaus|10 (9, 416, 402, 17 verts) + 1 Neuhaus|8.

The extra is the 12-vert Neuhaus|10. This is from our stitcher creating a small ring that osmium rejects (likely self-intersecting). And Krefeld is a ring assigned to a different name.

Both are cases where osmium's geometry validation catches bad rings. Let me add a simple self-intersection check for our assembled rings.


   466→// --- Parallel admin: assemble outer rings from collected way geometries ---
   467→
   468→static std::vector<std::vector<std::pair<double,double>>> assemble_outer_rings(
   469→    const std::vector<std::pair<int64_t, std::string>>& members,
   470→    const std::unordered_map<int64_t, ParsedData::WayGeometry>& way_geoms)

// --- Parallel admin: assemble outer rings from collected way geometries ---

static std::vector<std::vector<std::pair<double,double>>> assemble_outer_rings(
    const std::vector<std::pair<int64_t, std::string>>& members,
    const std::unordered_map<int64_t, ParsedData::WayGeometry>& way_geoms)


Edit

// --- Parallel admin: assemble outer rings from collected way geometries ---

// --- Ring self-intersection check (matches osmium's segment crossing detection) ---

static bool segments_intersect(double ax1, double ay1, double ax2, double ay2,
                                double bx1, double by1, double bx2, double by2) {
    // Check if segment (a1,a2) crosses segment (b1,b2)
    // Using cross product orientation test
    auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) -> double {
        return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);
    };
    double d1 = cross(bx1, by1, bx2, by2, ax1, ay1);
    double d2 = cross(bx1, by1, bx2, by2, ax2, ay2);
    double d3 = cross(ax1, ay1, ax2, ay2, bx1, by1);
    double d4 = cross(ax1, ay1, ax2, ay2, bx2, by2);
    if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
        ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
        return true;
    }
    return false;
}

static bool ring_has_self_intersection(const std::vector<std::pair<double,double>>& ring) {
    // O(n^2) check — only called for assembled rings (typically < 1000 vertices)
    size_t n = ring.size();
    if (n < 4) return false;
    for (size_t i = 0; i + 1 < n; i++) {
        for (size_t j = i + 2; j + 1 < n; j++) {
            // Skip adjacent segments (they share an endpoint)
            if (j == i + 1 || (i == 0 && j == n - 2)) continue;
            if (segments_intersect(ring[i].first, ring[i].second,
                                    ring[i+1].first, ring[i+1].second,
                                    ring[j].first, ring[j].second,
                                    ring[j+1].first, ring[j+1].second)) {
                return true;
            }
        }
    }
    return false;
}

// --- Parallel admin: assemble outer rings from collected way geometries ---