// --- New tests: coord_key edge cases ---

void test_coord_key_equator_prime_meridian() {
    TEST("coord_key at equator/prime meridian (0,0)");
    int64_t k = coord_key(0.0, 0.0);
    ASSERT_TRUE(k != coord_key(0.0001, 0.0));
    ASSERT_TRUE(k != coord_key(0.0, 0.0001));
    PASS();
}

void test_coord_key_max_values() {
    TEST("coord_key at extreme coordinates");
    // North pole area
    int64_t k1 = coord_key(89.9999, 179.9999);
    int64_t k2 = coord_key(-89.9999, -179.9999);
    ASSERT_TRUE(k1 != k2);
    // Very close but distinct
    ASSERT_TRUE(coord_key(51.5000001, -0.1) != coord_key(51.5000002, -0.1));
    PASS();
}

void test_coord_key_precision_boundary() {
    TEST("coord_key precision at 1e-7 degree boundary");
    // 1e-7 degrees is ~1cm at equator — our quantization boundary
    // These should be equal (within quantization)
    ASSERT_EQ(coord_key(51.50000004, -0.1), coord_key(51.50000004, -0.1));
    // These should differ (different quantized values)
    ASSERT_TRUE(coord_key(51.5000000, -0.1) != coord_key(51.5000002, -0.1));
    PASS();
}

// --- New tests: segments_intersect ---

void test_segments_parallel() {
    TEST("segments_intersect: parallel segments don't intersect");
    ASSERT_TRUE(!segments_intersect(0,0, 1,0,  0,1, 1,1));
    PASS();
}

void test_segments_collinear() {
    TEST("segments_intersect: collinear overlapping segments");
    // Collinear segments: our cross-product test returns false (no proper crossing)
    ASSERT_TRUE(!segments_intersect(0,0, 2,0,  1,0, 3,0));
    PASS();
}

void test_segments_t_junction() {
    TEST("segments_intersect: T-junction (endpoint on segment)");
    // Endpoint touching — not a proper crossing
    ASSERT_TRUE(!segments_intersect(0,0, 2,0,  1,0, 1,1));
    PASS();
}

void test_segments_clear_cross() {
    TEST("segments_intersect: clear X crossing");
    ASSERT_TRUE(segments_intersect(0,0, 1,1,  0,1, 1,0));
    PASS();
}

void test_segments_no_cross() {
    TEST("segments_intersect: clearly separate segments");
    ASSERT_TRUE(!segments_intersect(0,0, 1,0,  2,2, 3,2));
    PASS();
}

// --- New tests: ring_has_self_intersection ---

void test_self_intersection_triangle() {
    TEST("self-intersection: triangle has no crossing");
    std::vector<std::pair<double,double>> tri = {
        {0, 0}, {1, 0}, {0.5, 1}, {0, 0}
    };
    ASSERT_TRUE(!ring_has_self_intersection(tri));
    PASS();
}

void test_self_intersection_too_few_points() {
    TEST("self-intersection: fewer than 4 points returns false");
    std::vector<std::pair<double,double>> tiny = {{0,0}, {1,0}, {0,0}};
    ASSERT_TRUE(!ring_has_self_intersection(tiny));
    PASS();
}

void test_self_intersection_large_ring_clean() {
    TEST("self-intersection: large clean ring (sweep-line path)");
    // Create a 100-point ring (circle) — no self-intersection
    std::vector<std::pair<double,double>> ring;
    for (int i = 0; i < 100; i++) {
        double angle = 2.0 * M_PI * i / 100.0;
        ring.push_back({std::cos(angle), std::sin(angle)});
    }
    ring.push_back(ring.front());
    ASSERT_TRUE(!ring_has_self_intersection(ring));
    PASS();
}

void test_self_intersection_large_ring_crossed() {
    TEST("self-intersection: large ring with crossing");
    // Circle with two swapped points to create crossing
    std::vector<std::pair<double,double>> ring;
    for (int i = 0; i < 100; i++) {
        double angle = 2.0 * M_PI * i / 100.0;
        ring.push_back({std::cos(angle), std::sin(angle)});
    }
    ring.push_back(ring.front());
    // Swap two non-adjacent points to create crossing
    std::swap(ring[20], ring[80]);
    ASSERT_TRUE(ring_has_self_intersection(ring));
    PASS();
}

// --- New tests: polygon_area ---

void test_polygon_area_triangle() {
    TEST("polygon_area: right triangle");
    std::vector<std::pair<double,double>> tri = {
        {0, 0}, {2, 0}, {0, 3}, {0, 0}
    };
    float area = polygon_area(tri);
    ASSERT_TRUE(std::abs(area - 3.0f) < 1e-6f);
    PASS();
}

void test_polygon_area_degenerate() {
    TEST("polygon_area: degenerate (line) has zero area");
    std::vector<std::pair<double,double>> line = {
        {0, 0}, {1, 0}, {2, 0}, {0, 0}
    };
    float area = polygon_area(line);
    ASSERT_TRUE(std::abs(area) < 1e-6f);
    PASS();
}

void test_polygon_area_clockwise() {
    TEST("polygon_area: clockwise and counterclockwise same result");
    std::vector<std::pair<double,double>> ccw = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    std::vector<std::pair<double,double>> cw = {
        {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}
    };
    ASSERT_TRUE(std::abs(polygon_area(ccw) - polygon_area(cw)) < 1e-6f);
    PASS();
}

// --- New tests: simplify_polygon ---

void test_simplify_already_small() {
    TEST("simplify_polygon: already below threshold returns unchanged");
    std::vector<std::pair<double,double>> pts = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    auto result = simplify_polygon(pts, 10);
    ASSERT_EQ(result.size(), pts.size());
    PASS();
}

void test_simplify_reduces_points() {
    TEST("simplify_polygon: large polygon gets reduced");
    // Create a 200-point near-circle
    std::vector<std::pair<double,double>> ring;
    for (int i = 0; i < 200; i++) {
        double angle = 2.0 * M_PI * i / 200.0;
        ring.push_back({std::cos(angle), std::sin(angle)});
    }
    ring.push_back(ring.front());
    auto result = simplify_polygon(ring, 50);
    ASSERT_TRUE(result.size() <= 50);
    ASSERT_TRUE(result.size() >= 3);
    PASS();
}

// --- New tests: parse_house_number ---

void test_parse_house_number_basic() {
    TEST("parse_house_number: basic number");
    ASSERT_EQ(parse_house_number("42"), 42u);
    PASS();
}

void test_parse_house_number_alpha_suffix() {
    TEST("parse_house_number: number with letter suffix");
    ASSERT_EQ(parse_house_number("42a"), 42u);
    ASSERT_EQ(parse_house_number("123B"), 123u);
    PASS();
}

void test_parse_house_number_empty() {
    TEST("parse_house_number: empty string");
    ASSERT_EQ(parse_house_number(""), 0u);
    PASS();
}

void test_parse_house_number_null() {
    TEST("parse_house_number: null pointer");
    ASSERT_EQ(parse_house_number(nullptr), 0u);
    PASS();
}

void test_parse_house_number_no_digits() {
    TEST("parse_house_number: no leading digits");
    ASSERT_EQ(parse_house_number("abc"), 0u);
    PASS();
}

void test_parse_house_number_range() {
    TEST("parse_house_number: range string");
    ASSERT_EQ(parse_house_number("10-12"), 10u);
    PASS();
}

// --- New tests: is_included_highway ---

void test_highway_excluded_types() {
    TEST("is_included_highway: excluded types");
    ASSERT_TRUE(!is_included_highway("footway"));
    ASSERT_TRUE(!is_included_highway("path"));
    ASSERT_TRUE(!is_included_highway("track"));
    ASSERT_TRUE(!is_included_highway("steps"));
    ASSERT_TRUE(!is_included_highway("cycleway"));
    ASSERT_TRUE(!is_included_highway("service"));
    ASSERT_TRUE(!is_included_highway("pedestrian"));
    ASSERT_TRUE(!is_included_highway("bridleway"));
    ASSERT_TRUE(!is_included_highway("construction"));
    PASS();
}

void test_highway_included_types() {
    TEST("is_included_highway: included types");
    ASSERT_TRUE(is_included_highway("residential"));
    ASSERT_TRUE(is_included_highway("primary"));
    ASSERT_TRUE(is_included_highway("secondary"));
    ASSERT_TRUE(is_included_highway("tertiary"));
    ASSERT_TRUE(is_included_highway("motorway"));
    ASSERT_TRUE(is_included_highway("trunk"));
    ASSERT_TRUE(is_included_highway("living_street"));
    ASSERT_TRUE(is_included_highway("unclassified"));
    PASS();
}

// --- New tests: StringPool ---

void test_string_pool_basic() {
    TEST("StringPool: basic intern and retrieve");
    StringPool pool;
    uint32_t id1 = pool.intern("hello");
    uint32_t id2 = pool.intern("world");
    ASSERT_TRUE(id1 != id2);
    ASSERT_EQ(std::string(pool.data().data() + id1), std::string("hello"));
    ASSERT_EQ(std::string(pool.data().data() + id2), std::string("world"));
    PASS();
}

void test_string_pool_dedup() {
    TEST("StringPool: duplicate strings return same offset");
    StringPool pool;
    uint32_t id1 = pool.intern("test");
    uint32_t id2 = pool.intern("test");
    ASSERT_EQ(id1, id2);
    PASS();
}

void test_string_pool_empty_string() {
    TEST("StringPool: empty string");
    StringPool pool;
    uint32_t id = pool.intern("");
    ASSERT_EQ(std::string(pool.data().data() + id), std::string(""));
    ASSERT_EQ(pool.data()[id], '\0');
    PASS();
}

void test_string_pool_order() {
    TEST("StringPool: first intern gets offset 0");
    StringPool pool;
    uint32_t id = pool.intern("first");
    ASSERT_EQ(id, 0u);
    // Second string starts after "first\0" = 6 bytes
    uint32_t id2 = pool.intern("second");
    ASSERT_EQ(id2, 6u);
    PASS();
}

// --- New tests: ring_has_duplicate_coords edge cases ---

void test_duplicate_coords_closing_point() {
    TEST("duplicate coords: closing point not counted as duplicate");
    // First and last point are the same — this is normal for a closed ring
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0, 0}
    };
    ASSERT_TRUE(!ring_has_duplicate_coords(ring));
    PASS();
}

void test_duplicate_coords_two_point() {
    TEST("duplicate coords: two-point ring");
    std::vector<std::pair<double,double>> ring = {{0, 0}, {0, 0}};
    // Only one point checked (skip closing), no duplicate
    ASSERT_TRUE(!ring_has_duplicate_coords(ring));
    PASS();
}

// --- New tests: dp_simplify ---

void test_dp_simplify_straight_line() {
    TEST("dp_simplify: points on a straight line collapse");
    std::vector<std::pair<double,double>> pts = {
        {0, 0}, {0.5, 0}, {1, 0}, {1.5, 0}, {2, 0}
    };
    std::vector<bool> keep(pts.size(), false);
    keep[0] = true;
    keep[pts.size() - 1] = true;
    dp_simplify(pts, 0, pts.size() - 1, 0.01, keep);
    // Only first and last should be kept — interior points are on the line
    int count = 0;
    for (bool k : keep) if (k) count++;
    ASSERT_EQ(count, 2);
    PASS();
}

void test_dp_simplify_preserves_peak() {
    TEST("dp_simplify: preserves peak point");
    std::vector<std::pair<double,double>> pts = {
        {0, 0}, {0.5, 1}, {1, 0}
    };
    std::vector<bool> keep(pts.size(), false);
    keep[0] = true;
    keep[pts.size() - 1] = true;
    dp_simplify(pts, 0, pts.size() - 1, 0.01, keep);
    // The peak at (0.5, 1) should be kept
    ASSERT_TRUE(keep[1]);
    PASS();
}

// --- New tests: binary structs ---

void test_way_header_size() {
    TEST("WayHeader struct size");
    // Ensure struct is packed as expected
    ASSERT_TRUE(sizeof(WayHeader) > 0);
    ASSERT_TRUE(sizeof(WayHeader) <= 16); // should be compact
    PASS();
}

void test_admin_polygon_struct() {
    TEST("AdminPolygon country_code encoding");
    AdminPolygon ap{};
    const char* cc = "US";
    ap.country_code = static_cast<uint16_t>((cc[0] << 8) | cc[1]);
    ASSERT_EQ(ap.country_code >> 8, (uint16_t)'U');
    ASSERT_EQ(ap.country_code & 0xFF, (uint16_t)'S');
    PASS();
}

void test_interior_flag() {
    TEST("INTERIOR_FLAG and ID_MASK are complementary");
    uint32_t id = 12345;
    uint32_t flagged = id | INTERIOR_FLAG;
    ASSERT_EQ(flagged & ID_MASK, id);
    ASSERT_TRUE(flagged & INTERIOR_FLAG);
    ASSERT_TRUE(!(id & INTERIOR_FLAG));
    PASS();
}

// --- Main ---

int main() {
    std::cerr << "Running unit tests..." << std::endl;

    // Original tests
    test_coord_key_basic();
    test_coord_key_split_nodes();
    test_coord_key_negative_coords();
    test_self_intersection_simple_square();
    test_self_intersection_bowtie();
    test_duplicate_coords_detection();
    test_no_duplicate_coords_simple_ring();
    test_border_line_name_pattern();
    test_string_separator_bug();
    test_cell_item_pair_sorting();
    test_parallel_merge_join_correctness();
    test_ring_area_computation();

    // New: coord_key edge cases
    test_coord_key_equator_prime_meridian();
    test_coord_key_max_values();
    test_coord_key_precision_boundary();

    // New: segments_intersect
    test_segments_parallel();
    test_segments_collinear();
    test_segments_t_junction();
    test_segments_clear_cross();
    test_segments_no_cross();

    // New: ring_has_self_intersection
    test_self_intersection_triangle();
    test_self_intersection_too_few_points();
    test_self_intersection_large_ring_clean();
    test_self_intersection_large_ring_crossed();

    // New: polygon_area
    test_polygon_area_triangle();
    test_polygon_area_degenerate();
    test_polygon_area_clockwise();

    // New: simplify_polygon
    test_simplify_already_small();
    test_simplify_reduces_points();

    // New: parse_house_number
    test_parse_house_number_basic();
    test_parse_house_number_alpha_suffix();
    test_parse_house_number_empty();
    test_parse_house_number_null();
    test_parse_house_number_no_digits();
    test_parse_house_number_range();

    // New: is_included_highway
    test_highway_excluded_types();
    test_highway_included_types();

    // New: StringPool
    test_string_pool_basic();
    test_string_pool_dedup();
    test_string_pool_empty_string();
    test_string_pool_order();

    // New: ring_has_duplicate_coords edge cases
    test_duplicate_coords_closing_point();
    test_duplicate_coords_two_point();

    // New: dp_simplify
    test_dp_simplify_straight_line();
    test_dp_simplify_preserves_peak();

    // New: binary structs
    test_way_header_size();
    test_admin_polygon_struct();
    test_interior_flag();

    std::cerr << std::endl;
    std::cerr << tests_passed << "/" << tests_run << " tests passed." << std::endl;

    return (tests_passed == tests_run) ? 0 : 1;
}

// Unit tests for ring assembly algorithms.
// Tests the key edge cases that caused bugs during development:
// - Coordinate-based endpoint matching (split/replaced nodes)
// - Backtracking at branch points
// - Closed-way admin polygon detection
// - Inner-role way retry
// - Self-intersection detection
// - Border-line relation filtering
// - Duplicate coordinate detection (hole merging)
//
// Build: g++ -O2 -std=c++17 -o test_ring_assembly test_ring_assembly.cpp
// Run:   ./test_ring_assembly

#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

#include "../src/types.h"
#include "../src/string_pool.h"
#include "../src/geometry.h"

// --- Test helpers ---

static int tests_run = 0;
static int tests_passed = 0;

#define TEST(name) do { \
    tests_run++; \
    std::cerr << "  TEST: " << name << "... "; \
} while(0)

#define PASS() do { \
    tests_passed++; \
    std::cerr << "PASS" << std::endl; \
} while(0)

#define FAIL(msg) do { \
    std::cerr << "FAIL: " << msg << std::endl; \
} while(0)

template<typename T> std::string to_str(const T& v) { return std::to_string(v); }
inline std::string to_str(const std::string& v) { return "\"" + v + "\""; }
inline std::string to_str(const char* v) { return std::string("\"") + v + "\""; }

#define ASSERT_EQ(a, b) do { \
    if ((a) != (b)) { FAIL(#a " != " #b " (" + to_str(a) + " vs " + to_str(b) + ")"); return; } \
} while(0)

#define ASSERT_TRUE(cond) do { \
    if (!(cond)) { FAIL(#cond " is false"); return; } \
} while(0)

// --- Tests ---

void test_coord_key_basic() {
    TEST("coord_key basic");
    // Same coordinates should produce same key
    ASSERT_EQ(coord_key(51.5, -0.1), coord_key(51.5, -0.1));
    // Different coordinates should produce different keys
    ASSERT_TRUE(coord_key(51.5, -0.1) != coord_key(51.5, -0.2));
    ASSERT_TRUE(coord_key(51.5, -0.1) != coord_key(51.6, -0.1));
    PASS();
}

void test_coord_key_split_nodes() {
    TEST("coord_key handles split/replaced nodes at same location");
    // Two different OSM nodes at the same coordinates should have the same key
    // (even though they have different node IDs)
    double lat = 49.5576001, lng = 8.8074189;
    int64_t k1 = coord_key(lat, lng);
    int64_t k2 = coord_key(lat, lng);
    ASSERT_EQ(k1, k2);
    PASS();
}

void test_coord_key_negative_coords() {
    TEST("coord_key with negative coordinates");
    // Southern/western hemisphere
    int64_t k1 = coord_key(-33.8688, 151.2093); // Sydney
    int64_t k2 = coord_key(-33.8688, 151.2093);
    ASSERT_EQ(k1, k2);
    // Different cities
    ASSERT_TRUE(coord_key(-33.8688, 151.2093) != coord_key(-22.9068, -43.1729));
    PASS();
}

void test_self_intersection_simple_square() {
    TEST("self-intersection: simple square has no crossing");
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}
    };
    ASSERT_TRUE(!ring_has_self_intersection(square));
    PASS();
}

void test_self_intersection_bowtie() {
    TEST("self-intersection: bowtie (figure-8) detected");
    std::vector<std::pair<double,double>> bowtie = {
        {0, 0}, {1, 1}, {1, 0}, {0, 1}, {0, 0}
    };
    ASSERT_TRUE(ring_has_self_intersection(bowtie));
    PASS();
}

void test_duplicate_coords_detection() {
    TEST("duplicate coordinate detection in ring");
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0.5, 0.5}, {0, 1}, {0.5, 0.5}, {1, 1}, {0, 0}
    };
    ASSERT_TRUE(ring_has_duplicate_coords(ring));
    PASS();
}

void test_no_duplicate_coords_simple_ring() {
    TEST("no duplicate coords in simple ring");
    std::vector<std::pair<double,double>> ring = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    ASSERT_TRUE(!ring_has_duplicate_coords(ring));
    PASS();
}

void test_border_line_name_pattern() {
    TEST("border-line relation name detection");
    // Level 2 relations with " - " or " — " in name are border lines, not countries
    auto is_border_line = [](const std::string& name) {
        return name.find(" - ") != std::string::npos ||
               name.find(" \xe2\x80\x94 ") != std::string::npos; // em dash
    };
    ASSERT_TRUE(is_border_line("Deutschland - Österreich"));
    ASSERT_TRUE(is_border_line("France - Deutschland"));
    ASSERT_TRUE(is_border_line("Deutschland \xe2\x80\x94 Schweiz / Suisse / Svizerra"));
    ASSERT_TRUE(!is_border_line("Deutschland"));
    ASSERT_TRUE(!is_border_line("Akrotiri and Dhekelia")); // not a border line
    ASSERT_TRUE(!is_border_line("Österreich"));
    PASS();
}

void test_string_separator_bug() {
    TEST("string null separator (the \\0 bug)");
    // The original bug: std::string("x") + "\0" doesn't embed a null
    std::string bad = std::string("42") + "\0" + std::string("Main St");
    // This produces "42Main St" — the \0 is an empty string in the concatenation
    // The fix uses std::pair<std::string, std::string> instead

    // Verify the bug exists
    ASSERT_EQ(bad, std::string("42Main St")); // NOT "42\0Main St"
    ASSERT_EQ(bad.size(), 9u); // "42Main St" = 9 chars (no null embedded)

    // Verify the fix: pair stores them separately
    std::pair<std::string, std::string> fixed = {"42", "Main St"};
    ASSERT_EQ(fixed.first, "42");
    ASSERT_EQ(fixed.second, "Main St");
    PASS();
}

void test_cell_item_pair_sorting() {
    TEST("CellItemPair sorting by cell_id then item_id");
    std::vector<CellItemPair> pairs = {
        {100, 3}, {50, 1}, {100, 1}, {50, 2}, {100, 2}
    };
    std::sort(pairs.begin(), pairs.end(), [](const CellItemPair& a, const CellItemPair& b) {
        return a.cell_id < b.cell_id || (a.cell_id == b.cell_id && a.item_id < b.item_id);
    });
    ASSERT_EQ(pairs[0].cell_id, 50u);
    ASSERT_EQ(pairs[0].item_id, 1u);
    ASSERT_EQ(pairs[1].cell_id, 50u);
    ASSERT_EQ(pairs[1].item_id, 2u);
    ASSERT_EQ(pairs[2].cell_id, 100u);
    ASSERT_EQ(pairs[2].item_id, 1u);
    PASS();
}

void test_parallel_merge_join_correctness() {
    TEST("merge-join produces correct offsets");
    // Simulate: sorted_cells has cells 10,20,30,40,50
    // sorted_pairs has items for cells 10,30,50
    std::vector<uint64_t> sorted_cells = {10, 20, 30, 40, 50};
    std::vector<CellItemPair> sorted_pairs = {
        {10, 1}, {10, 2},
        {30, 3},
        {50, 4}, {50, 5}, {50, 6}
    };

    // Simulate merge-join
    const uint32_t NO_DATA = 0xFFFFFFFF;
    std::vector<uint32_t> offsets(sorted_cells.size(), NO_DATA);
    uint32_t current = 0;
    size_t pi = 0;
    for (uint32_t si = 0; si < sorted_cells.size() && pi < sorted_pairs.size(); si++) {
        if (sorted_cells[si] < sorted_pairs[pi].cell_id) continue;
        while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id < sorted_cells[si]) pi++;
        if (pi >= sorted_pairs.size() || sorted_pairs[pi].cell_id != sorted_cells[si]) continue;
        offsets[si] = current;
        size_t start = pi;
        while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id == sorted_cells[si]) pi++;
        current += sizeof(uint16_t) + (pi - start) * sizeof(uint32_t);
    }

    // Cell 10 (index 0) should have offset 0
    ASSERT_EQ(offsets[0], 0u);
    // Cell 20 (index 1) has no items
    ASSERT_EQ(offsets[1], NO_DATA);
    // Cell 30 (index 2) should have offset after cell 10's data
    uint32_t expected_30 = sizeof(uint16_t) + 2 * sizeof(uint32_t); // count(2) + 2 items
    ASSERT_EQ(offsets[2], expected_30);
    // Cell 40 (index 3) has no items
    ASSERT_EQ(offsets[3], NO_DATA);
    // Cell 50 (index 4) should have offset after cells 10+30
    uint32_t expected_50 = expected_30 + sizeof(uint16_t) + 1 * sizeof(uint32_t);
    ASSERT_EQ(offsets[4], expected_50);
    PASS();
}

void test_ring_area_computation() {
    TEST("ring area computation (shoelace formula)");
    std::vector<std::pair<double,double>> square = {
        {0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}
    };
    float area = polygon_area(square);
    ASSERT_TRUE(std::abs(area - 1.0f) < 1e-6f);
    PASS();
}

// --- Main ---

int main() {
    std::cerr << "Running ring assembly unit tests..." << std::endl;

    test_coord_key_basic();
    test_coord_key_split_nodes();
    test_coord_key_negative_coords();
    test_self_intersection_simple_square();
    test_self_intersection_bowtie();
    test_duplicate_coords_detection();
    test_no_duplicate_coords_simple_ring();
    test_border_line_name_pattern();
    test_string_separator_bug();
    test_cell_item_pair_sorting();
    test_parallel_merge_join_correctness();
    test_ring_area_computation();

    std::cerr << std::endl;
    std::cerr << tests_passed << "/" << tests_run << " tests passed." << std::endl;

    return (tests_passed == tests_run) ? 0 : 1;
}

+// --- New tests: coord_key edge cases ---

+    TEST("coord_key at equator/prime meridian (0,0)");

+    TEST("coord_key at extreme coordinates");

+    TEST("coord_key precision at 1e-7 degree boundary");

+    // 1e-7 degrees is ~1cm at equator — our quantization boundary

+    // These should be equal (within quantization)

+    // These should differ (different quantized values)

+// --- New tests: segments_intersect ---

+    TEST("segments_intersect: parallel segments don't intersect");

+    ASSERT_TRUE(!segments_intersect(0,0, 1,0,  0,1, 1,1));

+    TEST("segments_intersect: collinear overlapping segments");

+    // Collinear segments: our cross-product test returns false (no proper crossing)

+    ASSERT_TRUE(!segments_intersect(0,0, 2,0,  1,0, 3,0));

+    TEST("segments_intersect: T-junction (endpoint on segment)");

+    // Endpoint touching — not a proper crossing

+    ASSERT_TRUE(!segments_intersect(0,0, 2,0,  1,0, 1,1));

+    TEST("segments_intersect: clear X crossing");

+    ASSERT_TRUE(segments_intersect(0,0, 1,1,  0,1, 1,0));

+    TEST("segments_intersect: clearly separate segments");

+    ASSERT_TRUE(!segments_intersect(0,0, 1,0,  2,2, 3,2));

+// --- New tests: ring_has_self_intersection ---

+    TEST("self-intersection: triangle has no crossing");

+    std::vector<std::pair<double,double>> tri = {

+    ASSERT_TRUE(!ring_has_self_intersection(tri));

+    TEST("self-intersection: fewer than 4 points returns false");

+    std::vector<std::pair<double,double>> tiny = {{0,0}, {1,0}, {0,0}};

+    ASSERT_TRUE(!ring_has_self_intersection(tiny));

+    TEST("self-intersection: large clean ring (sweep-line path)");

+    // Create a 100-point ring (circle) — no self-intersection

+    std::vector<std::pair<double,double>> ring;

+        double angle = 2.0 * M_PI * i / 100.0;

+        ring.push_back({std::cos(angle), std::sin(angle)});

+    ASSERT_TRUE(!ring_has_self_intersection(ring));

+    TEST("self-intersection: large ring with crossing");

+    // Circle with two swapped points to create crossing

+    // Swap two non-adjacent points to create crossing

+    ASSERT_TRUE(ring_has_self_intersection(ring));

+    TEST("polygon_area: right triangle");

+    ASSERT_TRUE(std::abs(area - 3.0f) < 1e-6f);

+    TEST("polygon_area: degenerate (line) has zero area");

+    std::vector<std::pair<double,double>> line = {

+    ASSERT_TRUE(std::abs(area) < 1e-6f);

+    TEST("polygon_area: clockwise and counterclockwise same result");

+    std::vector<std::pair<double,double>> ccw = {

+    std::vector<std::pair<double,double>> cw = {

+    ASSERT_TRUE(std::abs(polygon_area(ccw) - polygon_area(cw)) < 1e-6f);

+    TEST("simplify_polygon: already below threshold returns unchanged");

+    std::vector<std::pair<double,double>> pts = {

+    auto result = simplify_polygon(pts, 10);

+    ASSERT_EQ(result.size(), pts.size());

+    TEST("simplify_polygon: large polygon gets reduced");

+    std::vector<std::pair<double,double>> ring;

+        double angle = 2.0 * M_PI * i / 200.0;

+    auto result = simplify_polygon(ring, 50);

+// --- New tests: parse_house_number ---

+    TEST("parse_house_number: basic number");

+    ASSERT_EQ(parse_house_number("42"), 42u);

+    TEST("parse_house_number: number with letter suffix");

+    ASSERT_EQ(parse_house_number("42a"), 42u);

+    ASSERT_EQ(parse_house_number("123B"), 123u);

+    TEST("parse_house_number: empty string");

+    ASSERT_EQ(parse_house_number(""), 0u);

+    TEST("parse_house_number: null pointer");

+    ASSERT_EQ(parse_house_number(nullptr), 0u);

+    TEST("parse_house_number: no leading digits");

+    ASSERT_EQ(parse_house_number("abc"), 0u);

+    TEST("parse_house_number: range string");

+    ASSERT_EQ(parse_house_number("10-12"), 10u);

+// --- New tests: is_included_highway ---

+    TEST("is_included_highway: excluded types");

+    ASSERT_TRUE(!is_included_highway("footway"));

+    ASSERT_TRUE(!is_included_highway("path"));

+    ASSERT_TRUE(!is_included_highway("track"));

+    ASSERT_TRUE(!is_included_highway("steps"));

+    ASSERT_TRUE(!is_included_highway("cycleway"));

+    ASSERT_TRUE(!is_included_highway("service"));

+    ASSERT_TRUE(!is_included_highway("pedestrian"));

+    ASSERT_TRUE(!is_included_highway("bridleway"));

+    ASSERT_TRUE(!is_included_highway("construction"));

+    TEST("is_included_highway: included types");

+    ASSERT_TRUE(is_included_highway("residential"));

+    ASSERT_TRUE(is_included_highway("primary"));

+    ASSERT_TRUE(is_included_highway("secondary"));

+    ASSERT_TRUE(is_included_highway("tertiary"));

+    ASSERT_TRUE(is_included_highway("motorway"));

+    ASSERT_TRUE(is_included_highway("trunk"));

+    ASSERT_TRUE(is_included_highway("living_street"));

+    ASSERT_TRUE(is_included_highway("unclassified"));

+    TEST("StringPool: basic intern and retrieve");

+    uint32_t id1 = pool.intern("hello");

+    uint32_t id2 = pool.intern("world");

+    ASSERT_EQ(std::string(pool.data().data() + id1), std::string("hello"));

+    ASSERT_EQ(std::string(pool.data().data() + id2), std::string("world"));

+    TEST("StringPool: duplicate strings return same offset");

+    uint32_t id1 = pool.intern("test");

+    uint32_t id2 = pool.intern("test");

+    ASSERT_EQ(std::string(pool.data().data() + id), std::string(""));

+    TEST("StringPool: first intern gets offset 0");

+    uint32_t id = pool.intern("first");

+    // Second string starts after "first\0" = 6 bytes

+    uint32_t id2 = pool.intern("second");

+// --- New tests: ring_has_duplicate_coords edge cases ---

+    TEST("duplicate coords: closing point not counted as duplicate");

+    // First and last point are the same — this is normal for a closed ring

+    std::vector<std::pair<double,double>> ring = {

+    ASSERT_TRUE(!ring_has_duplicate_coords(ring));

+    TEST("duplicate coords: two-point ring");

+    std::vector<std::pair<double,double>> ring = {{0, 0}, {0, 0}};

+    // Only one point checked (skip closing), no duplicate

+    TEST("dp_simplify: points on a straight line collapse");

+    std::vector<std::pair<double,double>> pts = {

+    std::vector<bool> keep(pts.size(), false);

+    dp_simplify(pts, 0, pts.size() - 1, 0.01, keep);

+    // Only first and last should be kept — interior points are on the line

+    for (bool k : keep) if (k) count++;

+    TEST("dp_simplify: preserves peak point");

+    // The peak at (0.5, 1) should be kept

+    // Ensure struct is packed as expected

+    ASSERT_TRUE(sizeof(WayHeader) > 0);

+    ASSERT_TRUE(sizeof(WayHeader) <= 16); // should be compact

+    TEST("AdminPolygon country_code encoding");

+    ap.country_code = static_cast<uint16_t>((cc[0] << 8) | cc[1]);

+    ASSERT_EQ(ap.country_code >> 8, (uint16_t)'U');

+    ASSERT_EQ(ap.country_code & 0xFF, (uint16_t)'S');

+    TEST("INTERIOR_FLAG and ID_MASK are complementary");

+    uint32_t flagged = id | INTERIOR_FLAG;

+    ASSERT_TRUE(flagged & INTERIOR_FLAG);

+    ASSERT_TRUE(!(id & INTERIOR_FLAG));

-    std::cerr << "Running ring assembly unit tests..." << std::endl;

+    std::cerr << "Running unit tests..." << std::endl;

+    test_coord_key_equator_prime_meridian();

+    test_coord_key_precision_boundary();

+    test_self_intersection_too_few_points();

+    test_self_intersection_large_ring_clean();

+    test_self_intersection_large_ring_crossed();

+    test_parse_house_number_alpha_suffix();

+    test_parse_house_number_no_digits();

+    // New: ring_has_duplicate_coords edge cases

+    test_duplicate_coords_closing_point();


Bash

cd /root/traccar-geocoder/builder && g++ -O2 -std=c++17 -o build/test_ring_assembly test/test_ring_assembly.cpp 2>&1 && build/test_ring_assembly 2>&1

Build and run expanded unit tests


Running unit tests...
  TEST: coord_key basic... PASS
  TEST: coord_key handles split/replaced nodes at same location... PASS
  TEST: coord_key with negative coordinates... PASS
  TEST: self-intersection: simple square has no crossing... PASS
  TEST: self-intersection: bowtie (figure-8) detected... PASS
  TEST: duplicate coordinate detection in ring... PASS
  TEST: no duplicate coords in simple ring... PASS
  TEST: border-line relation name detection... PASS
  TEST: string null separator (the \0 bug)... PASS
  TEST: CellItemPair sorting by cell_id then item_id... PASS
  TEST: merge-join produces correct offsets... PASS
  TEST: ring area computation (shoelace formula)... PASS
  TEST: coord_key at equator/prime meridian (0,0)... PASS
  TEST: coord_key at extreme coordinates... PASS
  TEST: coord_key precision at 1e-7 degree boundary... PASS
  TEST: segments_intersect: parallel segments don't intersect... PASS
  TEST: segments_intersect: collinear overlapping segments... PASS
  TEST: segments_intersect: T-junction (endpoint on segment)... PASS
  TEST: segments_intersect: clear X crossing... PASS
  TEST: segments_intersect: clearly separate segments... PASS
  TEST: self-intersection: triangle has no crossing... PASS
  TEST: self-intersection: fewer than 4 points returns false... PASS
  TEST: self-intersection: large clean ring (sweep-line path)... PASS
  TEST: self-intersection: large ring with crossing... PASS
  TEST: polygon_area: right triangle... PASS
  TEST: polygon_area: degenerate (line) has zero area... PASS
  TEST: polygon_area: clockwise and counterclockwise same result... PASS
  TEST: simplify_polygon: already below threshold returns unchanged... PASS
  TEST: simplify_polygon: large polygon gets reduced... PASS
  TEST: parse_house_number: basic number... PASS
  TEST: parse_house_number: number with letter suffix... PASS
  TEST: parse_house_number: empty string... PASS
  TEST: parse_house_number: null pointer... PASS
  TEST: parse_house_number: no leading digits... PASS
  TEST: parse_house_number: range string... PASS
  TEST: is_included_highway: excluded types... PASS
  TEST: is_included_highway: included types... PASS
  TEST: StringPool: basic intern and retrieve... PASS
  TEST: StringPool: duplicate strings return same offset... PASS
  TEST: StringPool: empty string... PASS
  TEST: StringPool: first intern gets offset 0... PASS
  TEST: duplicate coords: closing point not counted as duplicate... PASS
  TEST: duplicate coords: two-point ring... PASS
  TEST: dp_simplify: points on a straight line collapse... PASS
  TEST: dp_simplify: preserves peak point... PASS
  TEST: WayHeader struct size... PASS
  TEST: AdminPolygon country_code encoding... PASS
  TEST: INTERIOR_FLAG and ID_MASK are complementary... PASS

48/48 tests passed.


All 48 tests pass (up from 12).


Updated task #2 status


Updated task #3 status


Now fix the precision bug in `resolve_interpolation_endpoints`. It uses `* 100000` (1e5) instead of `* 1e7`:


   735→        CoordKey start_key{
   736→            static_cast<int32_t>(start.lat * 100000),
   737→            static_cast<int32_t>(start.lng * 100000)
   738→        };
   739→        auto it_start = addr_by_coord.find(start_key);
   740→
   741→        const auto& end = data.interp_nodes[iw.node_offset + iw.node_count - 1];
   742→        CoordKey end_key{
   743→            static_cast<int32_t>(end.lat * 100000),
   744→            static_cast<int32_t>(end.lng * 100000)
   745→        };
   746→        auto it_end = addr_by_coord.find(end_key);
   747→
   748→        if (it_start != addr_by_coord.end()) {
   749→            const char* hn = data.string_pool.data().data() + data.addr_points[it_start->second].housenumber_id;
   750→            iw.start_number = parse_house_number(hn);
   751→        }
   752→        if (it_end != addr_by_coord.end()) {
   753→            const char* hn = data.string_pool.data().data() + data.addr_points[it_end->second].housenumber_id;
   754→            iw.end_number = parse_house_number(hn);
   755→        }
   756→
   757→        if (iw.start_number > 0 && iw.end_number > 0) resolved++;
   758→    }
   759→
   760→    std::cerr << "Resolved " << resolved << "/" << data.interp_ways.size()
   761→              << " interpolation ways" << std::endl;
   762→}
   763→
   764→// --- Deduplicate IDs per cell ---
   765→
   766→template<typename Map>
   767→static void deduplicate(Map& cell_map) {
   768→    for (auto& [cell_id, ids] : cell_map) {
   769→        std::sort(ids.begin(), ids.end());
   770→        ids.erase(std::unique(ids.begin(), ids.end()), ids.end());
   771→    }
   772→}
   773→
   774→// --- Cache serialization ---
   775→
   776→enum class SectionType : uint32_t {
   777→    STRING_POOL = 0,
   778→    WAYS = 1,
   779→    STREET_NODES = 2,
   780→    CELL_TO_WAYS = 3,
   781→    ADDR_POINTS = 4,
   782→    CELL_TO_ADDRS = 5,
   783→    INTERP_WAYS = 6,
   784→    INTERP_NODES = 7,
   785→    CELL_TO_INTERPS = 8,
   786→    ADMIN_POLYGONS = 9,
   787→    ADMIN_VERTICES = 10,
   788→    CELL_TO_ADMIN = 11,
   789→};
   790→
   791→static const char CACHE_MAGIC[8] = {'T','G','C','A','C','H','E','\0'};
   792→static const uint32_t CACHE_VERSION = 1;
   793→static const uint32_t CACHE_SECTION_COUNT = 12;
   794→

        CoordKey start_key{
            static_cast<int32_t>(start.lat * 100000),
            static_cast<int32_t>(start.lng * 100000)
        };
        auto it_start = addr_by_coord.find(start_key);

        const auto& end = data.interp_nodes[iw.node_offset + iw.node_count - 1];
        CoordKey end_key{
            static_cast<int32_t>(end.lat * 100000),
            static_cast<int32_t>(end.lng * 100000)
        };
        auto it_end = addr_by_coord.find(end_key);

        if (it_start != addr_by_coord.end()) {
            const char* hn = data.string_pool.data().data() + data.addr_points[it_start->second].housenumber_id;
            iw.start_number = parse_house_number(hn);
        }
        if (it_end != addr_by_coord.end()) {
            const char* hn = data.string_pool.data().data() + data.addr_points[it_end->second].housenumber_id;
            iw.end_number = parse_house_number(hn);
        }

        if (iw.start_number > 0 && iw.end_number > 0) resolved++;
    }

    std::cerr << "Resolved " << resolved << "/" << data.interp_ways.size()
              << " interpolation ways" << std::endl;
}

// --- Deduplicate IDs per cell ---

template<typename Map>
static void deduplicate(Map& cell_map) {
    for (auto& [cell_id, ids] : cell_map) {
        std::sort(ids.begin(), ids.end());
        ids.erase(std::unique(ids.begin(), ids.end()), ids.end());
    }
}

// --- Cache serialization ---

enum class SectionType : uint32_t {
    STRING_POOL = 0,
    WAYS = 1,
    STREET_NODES = 2,
    CELL_TO_WAYS = 3,
    ADDR_POINTS = 4,
    CELL_TO_ADDRS = 5,
    INTERP_WAYS = 6,
    INTERP_NODES = 7,
    CELL_TO_INTERPS = 8,
    ADMIN_POLYGONS = 9,
    ADMIN_VERTICES = 10,
    CELL_TO_ADMIN = 11,
};

static const char CACHE_MAGIC[8] = {'T','G','C','A','C','H','E','\0'};
static const uint32_t CACHE_VERSION = 1;
static const uint32_t CACHE_SECTION_COUNT = 12;


I can see the full function. The issue is that addr_points store coordinates as `float` (lines 720-726), and the interpolation node coords are also `float`. When we multiply a float by 100000, we get ~5 decimal places of precision. But addr_points were originally stored with `float` precision from `double` lat/lng values.

The key question: addr_points are stored as `float`, and interp_nodes are also `float`. Both come from the same float values. So the 100000 multiplier matches float precision (~1m). Using 1e7 would actually be too fine — `float` only has ~7 significant digits, so `float * 1e7` would have quantization noise.

Actually, the right fix is to make the interpolation endpoint resolution use the same precision as the storage format. Since both addr_points and interp_nodes use `float` coords, and `float` has ~7 decimal digits of precision, `* 100000` (5 digits after decimal) is actually reasonable for float. Using `* 1e7` would exceed float precision and cause spurious mismatches.

However, let me verify — let me check if addr_by_coord is built from float values too:


   710→    struct CoordKey {
   711→        int32_t lat;
   712→        int32_t lng;
   713→        bool operator==(const CoordKey& o) const { return lat == o.lat && lng == o.lng; }
   714→    };
   715→    struct CoordHash {
   716→        size_t operator()(const CoordKey& k) const {
   717→            return std::hash<int64_t>()(((int64_t)k.lat << 32) | (uint32_t)k.lng);
   718→        }
   719→    };
   720→
   721→    std::unordered_map<CoordKey, uint32_t, CoordHash> addr_by_coord;
   722→    for (uint32_t i = 0; i < data.addr_points.size(); i++) {
   723→        CoordKey key{
   724→            static_cast<int32_t>(data.addr_points[i].lat * 100000),
   725→            static_cast<int32_t>(data.addr_points[i].lng * 100000)
   726→        };
   727→        addr_by_coord[key] = i;
   728→    }
   729→
   730→    uint32_t resolved = 0;
   731→    for (auto& iw : data.interp_ways) {
   732→        if (iw.node_count < 2) continue;
   733→
   734→        const auto& start = data.interp_nodes[iw.node_offset];
   735→        CoordKey start_key{
   736→            static_cast<int32_t>(start.lat * 100000),
   737→            static_cast<int32_t>(start.lng * 100000)
   738→        };
   739→        auto it_start = addr_by_coord.find(start_key);

    struct CoordKey {
        int32_t lat;
        int32_t lng;
        bool operator==(const CoordKey& o) const { return lat == o.lat && lng == o.lng; }
    };
    struct CoordHash {
        size_t operator()(const CoordKey& k) const {
            return std::hash<int64_t>()(((int64_t)k.lat << 32) | (uint32_t)k.lng);
        }
    };

    std::unordered_map<CoordKey, uint32_t, CoordHash> addr_by_coord;
    for (uint32_t i = 0; i < data.addr_points.size(); i++) {
        CoordKey key{
            static_cast<int32_t>(data.addr_points[i].lat * 100000),
            static_cast<int32_t>(data.addr_points[i].lng * 100000)
        };
        addr_by_coord[key] = i;
    }

    uint32_t resolved = 0;
    for (auto& iw : data.interp_ways) {
        if (iw.node_count < 2) continue;

        const auto& start = data.interp_nodes[iw.node_offset];
        CoordKey start_key{
            static_cast<int32_t>(start.lat * 100000),
            static_cast<int32_t>(start.lng * 100000)
        };
        auto it_start = addr_by_coord.find(start_key);


After reviewing this more carefully, the `* 100000` precision is **not actually a bug**. Both sides (addr_points and interp_nodes) use `float` for coordinates, and both use the same `* 100000` multiplier. Since `float` only has ~7 significant digits, using `* 1e7` would actually introduce matching failures due to float quantization noise. The current code is correct — the matching just needs to be self-consistent, which it is.


TaskUpdate

Reviewed: not a bug. Both sides use float coords with consistent * 100000 multiplier. Using * 1e7 would exceed float precision and cause matching failures.


Updated task #3 description, status


Updated task #4 status


Now let me clean up the dead try/catch blocks and extract magic numbers. First, find the try/catch blocks in way processing:


188:    struct WayGeometry {
1941:                    struct WayGeomEntry {
1998:                                                try {
2046:                                                try {
2078:                                                try {
2111:                                            try {
2158:                                                            try {


No matches found


2001:                                                } catch (...) { all_valid = false; break; }
2053:                                                } catch (...) {}
2081:                                                } catch (...) { all_valid = false; break; }
2114:                                            } catch (...) {}
2163:                                                            } catch (...) { all_valid = false; break; }


These `try/catch (...)` blocks wrap `index.get()` calls. Looking at the `DenseIndex::get()` method — it just does an array access with bounds checking, and never throws. These catches are dead code. Let me remove them:


  1995→                                        if (wnodes.size() >= 2) {
  1996→                                            bool all_valid = true;
  1997→                                            for (const auto& nr : wnodes) {
  1998→                                                try {
  1999→                                                    auto loc = index.get(nr.positive_ref());
  2000→                                                    if (!loc.valid()) { all_valid = false; break; }
  2001→                                                } catch (...) { all_valid = false; break; }
  2002→                                            }
  2003→                                            if (all_valid) {
  2004→                                                const char* street = way.tags()["addr:street"];
  2005→                                                if (street) {
  2006→                                                    uint32_t interp_id = static_cast<uint32_t>(local.interp_ways.size());
  2007→                                                    uint32_t node_offset = static_cast<uint32_t>(local.interp_nodes.size());
  2008→
  2009→                                                    for (const auto& nr : wnodes) {

                                        if (wnodes.size() >= 2) {
                                            bool all_valid = true;
                                            for (const auto& nr : wnodes) {
                                                try {
                                                    auto loc = index.get(nr.positive_ref());
                                                    if (!loc.valid()) { all_valid = false; break; }
                                                } catch (...) { all_valid = false; break; }
                                            }
                                            if (all_valid) {
                                                const char* street = way.tags()["addr:street"];
                                                if (street) {
                                                    uint32_t interp_id = static_cast<uint32_t>(local.interp_ways.size());
                                                    uint32_t node_offset = static_cast<uint32_t>(local.interp_nodes.size());

                                                    for (const auto& nr : wnodes) {


I'll replace all the try/catch blocks with direct calls. There are 5 occurrences. Let me do them one by one:


Edit

                                            for (const auto& nr : wnodes) {
                                                try {
                                                    auto loc = index.get(nr.positive_ref());
                                                    if (!loc.valid()) { all_valid = false; break; }
                                                } catch (...) { all_valid = false; break; }
                                            }
                                            if (all_valid) {
                                                const char* street = way.tags()["addr:street"];
                                                if (street) {
                                                    uint32_t interp_id = static_cast<uint32_t>(local.interp_ways.size());

                                            for (const auto& nr : wnodes) {
                                                auto loc = index.get(nr.positive_ref());
                                                if (!loc.valid()) { all_valid = false; break; }
                                            }
                                            if (all_valid) {
                                                const char* street = way.tags()["addr:street"];
                                                if (street) {
                                                    uint32_t interp_id = static_cast<uint32_t>(local.interp_ways.size());