Conversation compacted


This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.

Summary:
1. Primary Request and Intent:
   The user requested building a parallel processing pipeline for their traccar-geocoder C++ index builder that processes OpenStreetMap PBF files. The core goals evolved through the session:
   - Fix bugs in the parallel admin boundary assembly to match osmium's sequential output
   - Achieve 100% parity with the baseline (pre-parallel) code for ways, addresses, and admin polygons
   - Optimize build performance (achieved 5 hours → 13.7 minutes, 22x speedup)
   - Write comprehensive tests (unit, integration, e2e)
   - Refactor the code to remove the sequential code path and clean up
   - Back up all work to node 3 since commits can't be pushed to GitHub (no SSH key)
   - The user explicitly stated: "My main priority is maintaining parity with the original code" and "we should not sacrifice any of the speed improvements"

2. Key Technical Concepts:
   - C++ parallel processing with std::thread, std::async, std::atomic, producer-consumer queues
   - OpenStreetMap PBF file format processing with libosmium
   - S2 geometry library for cell covering computation
   - Ring assembly algorithms (greedy, backtracking, coordinate-based matching)
   - Dense node array with MAP_NORESERVE mmap for lockless parallel writes
   - Parallel sort strategies (per-thread sort + tree merge vs k-way heap merge)
   - Hash map vs sorted-pair approaches for cell-to-item mapping
   - Merge-join algorithms for index file generation
   - Sweep-line segment intersection detection
   - SSH multiplexing, Tailscale VPN, OIDC authentication for remote nodes
   - StringPool interning for memory-efficient string storage

3. Files and Code Sections:
   - `/root/traccar-geocoder/builder/src/build_index.cpp` (3033 lines, main source)
     - The monolithic C++ file containing the entire builder pipeline
     - Major sections: S2 helpers, geometry utilities, ring assembly, string pool, cache serialization, continent filtering, index writing, main() with PBF processing
     - Key changes: removed BuildHandler class (-212 lines), removed sequential osmium path, added parallel tree merge, parallel chunked merge-join, cover_edge fast-path, combined Pass 2+3

   - `/root/traccar-geocoder/builder/test/test_ring_assembly.cpp` (328 lines)
     - 12 unit tests covering coord_key, self-intersection, duplicate coords, border-line detection, string separator bug, CellItemPair sorting, merge-join offsets, ring area

   - `/root/traccar-geocoder/builder/test/test_integration.sh`
     - Integration test: builds index twice, compares for determinism, checks all 14 files present and non-empty

   - `/root/traccar-geocoder/builder/test/test_e2e_queries.sh`
     - E2E test: builds index, starts Rust server, queries coordinates, validates results and auth

   - `/root/traccar-geocoder/builder/test/compare_indexes.cpp`
     - Semantic comparison tool with --semantic mode for cross-version comparison
     - Uses fingerprinting (exact coordinate or name+level) for multiset comparison

   - `/root/traccar-geocoder/builder/test/VALIDATION_REPORT.md`
     - Comprehensive documentation of all validation results, optimizations, and bugs fixed

   - `/root/traccar-geocoder/server/src/main.rs` - Rust reverse geocoding server (used for e2e testing)

   - Key code patterns:
     ```cpp
     // Coordinate key matching osmium's Location (int32_t nanodegrees)
     static int64_t coord_key(double lat, double lng) {
         int32_t ilat = static_cast<int32_t>(lat * 1e7 + (lat >= 0 ? 0.5 : -0.5));
         int32_t ilng = static_cast<int32_t>(lng * 1e7 + (lng >= 0 ? 0.5 : -0.5));
         return (static_cast<int64_t>(ilat) << 32) | static_cast<uint32_t>(ilng);
     }
     ```
     ```cpp
     // Phase timer
     static void log_phase(const char* name, std::chrono::steady_clock::time_point& t) {
         auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
             std::chrono::steady_clock::now() - t).count();
         std::cerr << "  [" << ms/1000 << "." << (ms%1000)/100 << "s] " << name << std::endl;
         t = std::chrono::steady_clock::now();
     }
     ```
     ```cpp
     // CellItemPair for flat-array S2 cell collection
     struct CellItemPair { uint64_t cell_id; uint32_t item_id; };
     ```

4. Errors and Fixes:
   - **String interleaving bug**: parallel way processing pushed way names, building addr strings, and interpolation strings into single `interned_strings` vector in interleaved order, but merge code read them grouped by type. Fix: split into `way_strings`, `addr_strings`, `interp_strings` vectors.
   - **Building address "\0" separator bug**: `std::string("42") + "\0" + std::string("Main St")` produces "42Main St" not "42\0Main St". Fix: use `std::pair<std::string,std::string>` instead.
   - **Node ID matching for ring stitching**: OSM split/replaced nodes have different IDs at same coordinates. Fix: coordinate-based matching using `coord_key()`.
   - **Closed-way admin polygons**: osmium creates areas from standalone closed ways tagged with boundary=administrative. Fix: detect during way processing and add as admin polygons.
   - **Inner-role way retry**: osmium ignores roles during assembly. Fix: when outer-only produces 0 rings and relation has no outer ways, retry with all roles. Filter results by checking for duplicate coordinates (prevents hole+outer merging).
   - **Level 2 border-line relations**: "Deutschland - Österreich" are border lines not country polygons. Fix: filter by name pattern containing " - " or " — ".
   - **Backtracking stall on planet**: depth limit of 10000 caused exponential explosion. Fix: reduced to budget of 100K calls per ring, 500K per relation, and max 30 sub-ways for retry.
   - **SSH connection issues**: Tailscale userspace networking, OIDC auth timing windows, SSH multiplexing. Fix: ControlMaster with background sleep process, tailscale nc proxy.
   - **Baseline way count difference**: node 2a's SparseFileArray produced 14K fewer ways than node 3. Fix: run baseline on same machine (node 3) for fair comparison.
   - **K-way merge heap overhead**: heap-based merge was slower than pairwise merge due to per-element heap operations. Fix: reverted to parallel tree merge.
   - **Batch string interning overhead**: collecting unique strings from 28 threads into hash sets added 31s. Fix: reverted to sequential interning (stays hot in L2 cache).
   - User feedback: "Check more often to avoid wasting time" - adjusted monitoring intervals
   - User feedback: "Use aria2 for downloads" - switched from wget to aria2c with 16 connections
   - User feedback: "Run on different nodes" - distributed builds across node 1, 2a, 2b, 3
   - User feedback: "Don't make changes that compromise accuracy" - reverted S2 bisection optimization
   - User feedback: "Keep me updated with what you are doing" - improved status reporting

5. Problem Solving:
   - Traced 12K admin polygon gap down to 10 missing boundaries through iterative fixes
   - Proved 10 remaining missing have zero impact on reverse geocoding via actual server queries
   - Identified and parallelized all major single-threaded bottlenecks using CPU monitoring
   - Attempted osmium-style segment graph assembler (63 missing, worse than greedy's 10) — abandoned
   - Planet build optimization: 5h → 40min → 18min → 14min → 13.7min through systematic profiling

6. All User Messages:
   - Initial context dump about project overview, nodes, current problems
   - "We need to setup tailscale and connect to the immich github tailnet"
   - "Add bypass permissions mode as the default permission mode for claude"
   - "Alright, lets go. Continue with the previous task"
   - "/effort max"
   - "We should also update the semantic comparison tool to evaluate that, even though the ways and addresses are different orders, that they are effectively the same between the two files"
   - "You have full access to node 2a and 2b as well as node 3..."
   - "dont do everythint on node 3, you have the extra nodes to allow you to do sequential processing"
   - "makensure to keep committing even though you cant push"
   - "could we also run the initial baseline build before any of our changes on one continent and then save those results as a pure baseline for us to compare against"
   - "fix the admin polygon issue and also validate that the results are the same between the original tool before we made any changes, and the new parallel version"
   - "are we able to also use those same heuristics?" (osmium's ring closing)
   - "also, why do we have extra ways, that doesn't seem right"
   - "yes, lets make it match 100% across all sections"
   - "what would it take to make those differences the same too?"
   - "can you give me a full explanation of what the 3 differences will actually entail when using this data in the reverse geocoder?"
   - "save this report in a file in the repo and then run a parallel vs baseline test on the entirety of europe"
   - "are ypu using aria for the download?"
   - "Can you explain the differences for the extra and missing? why are we missing some specifically?"
   - "what are the fixes for all of these?"
   - "yes, leta make it match exactly"
   - "lets reimplument osmiums method, aiming to keep parallelism high... Lets do this in a branch"
   - "Are we able to implement the other features we are missing from osmium in order to get the same count?"
   - "Yes, add it. Also can we add timings for each section..."
   - "Lets run planets"
   - "would there be a performance impact to doing the processing the same way as osmium?"
   - "Yes, implement it" (osmium segment approach)
   - "To be clear, we still want to be completely accurate compared to the baseline"
   - "Why does index file writing take so long and what can we do to improve that section?"
   - "could we write all the files at once to increase parallelisation of the deduplication process?"
   - "Lets look for more optimisations"
   - "Keep investigating more optimisations"
   - "Check status every 30 seconds"
   - "Is there any way to improve the PBF IO, either by reducing the amount of passes or something else?"
   - "What is the average cpu usage in this phase?"
   - "Would it be possible to run the build on the node claude is running on instead?"
   - "If not, you could try a smaller continent" / "You could specifically test albania"
   - "Node 3 is different, but you can use that for node 3 too"
   - "Stepping away now, continue your work, feel free to use any nodes as you wish"
   - "What section is running right now, it's single threaded again"
   - "You should use node 1 until node 3 comes back online"
   - "I think Europe should work too, check with your measured stats"
   - "Can you also do the same for some of the missing polygon areas?" (server query test)
   - "I am going to bed now, keep working through this..." (autonomous work instruction)
   - "what is the status?"
   - "How long did the parallel version take, and how long is this baseline version taking?"
   - "did we always have 2544 extra boundaries or are those new from these optimisations?"
   - "Can you give me some examples of co-ordinates that would be in some of these extra boundaries?" (server comparison test)
   - "what was the speedup vs the previous parallel build?"
   - "What was the improvement vs the last round?"
   - "Run full planet after this to see how much better it is"
   - "We should investigate any way we can remove any sections that are single threaded"
   - "Keep monitoring and find other places where we could optimise to run more parallel"
   - "Kill the current planet run as it will throw off our comparison numbers"
   - "A huge amount of the write phase seems to be single threaded and CPU bound"
   - "What downsides does not merging at all have?"
   - "Yes, implement this now" (k-way merge)
   - "Always keep me updated with what you are doing"
   - "Tackle all three, I am heading to bed now so work on it autonomously"
   - "Make sure you are committing after each fix"
   - "It's in a single threaded section now, what is it?"
   - "What phase are we in now, it's single threaded again"
   - "Monitor the CPU now..."
   - "Include the cpu stats like you were before"
   - "Another single core step"
   - "Explain the backtracking issue again?"
   - "Keep monitoring and report after it's done"
   - "What is it doing now which is single threaded?"
   - "It is only using a single core at 100% cpu right now"
   - "Check more often, don't wait so long and risk the machine sitting there doing nothing for a while"
   - "still feels very slow for that size of file, this server is running on SSDs, what is causing the slowdown?"
   - "once you are totally done with performance improvements i would like you to write unit, integration, e2e tests... then do a significant refactor to clean up the code... copy this entire git repo to node3 as a backup"
   - "why did you stop?"
   - "we can also remove the sequential code now and make it always parallel"
   - "we can just remove the flag entirely as nothing uses this tool externally yet"

7. Pending Tasks:
   - The planning agent completed with a comprehensive refactoring plan identifying:
     - 38 additional unit tests to write
     - 9 modules to extract from the monolithic file
     - Multiple code duplication issues to fix
     - Magic numbers to name as constants
     - A potential precision bug in resolve_interpolation_endpoints (uses 100000 vs 1e7)
     - Dead try/catch blocks to remove
   - Push commits to GitHub when SSH key is available
   - Further code refactoring per the planning agent's analysis (extract testable utility functions, split main() into smaller functions, extract modules to separate files)
   - More test coverage (the planning agent identified 38 additional test cases)

8. Current Work:
   The most recent work was:
   - Removed the sequential osmium admin assembly code path (BuildHandler class, MultipolygonManager, --parallel-admin flags)
   - Updated integration test to test determinism instead of parallel vs sequential
   - Verified planet builds in 13.7 min with correct output after cleanup
   - Committed and backed up to node 3 (100 commits total)
   - The planning agent just completed with a comprehensive refactoring plan
   - The user's last explicit requests were: write tests, refactor code, back up to node 3, remove sequential code path

9. Optional Next Step:
   Based on the planning agent's completed analysis and the user's request to "write unit, integration, e2e tests, whatever is necessary to validate all of this complicated code" and "do a significant refactor to clean up the code", the next steps are:
   1. Implement the 38 additional unit tests identified by the planning agent
   2. Extract utility functions into separate headers (coord_key, geometry, StringPool, binary structs)
   3. Fix the identified potential precision bug in resolve_interpolation_endpoints
   4. Remove dead try/catch blocks from way processing lambda
   5. Extract magic numbers into named constants

   The planning agent recommended the sequence: "Phase 1: Extract testable utility functions... Phase 2: Write unit tests for extracted utilities... Phase 3: Extract larger modules... Phase 4: Refactor main()"

If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: /root/.claude/projects/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9.jsonl
Continue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, do not preface with "I'll continue" or similar. Pick up the last task as if the break never happened.


Agent

Explore current codebase state

Prompt:
Quickly explore the traccar-geocoder builder directory structure. I need to know:
1. All files in /root/traccar-geocoder/builder/ recursively
2. The current line count of build_index.cpp
3. Any existing header files or separate modules
4. The CMakeLists.txt or Makefile content


Quickly explore the traccar-geocoder builder directory structure. I need to know:
1. All files in /root/traccar-geocoder/builder/ recursively
2. The current line count of build_index.cpp
3. Any existing header files or separate modules
4. The CMakeLists.txt or Makefile content

Prompt:
Quickly explore the traccar-geocoder builder directory structure. I need to know:
1. All files in /root/traccar-geocoder/builder/ recursively
2. The current line count of build_index.cpp
3. Any existing header files or separate modules
4. The CMakeLists.txt or Makefile content


     1→#include <algorithm>
     2→#include <chrono>
     3→#include <atomic>
     4→#include <cmath>
     5→#include <condition_variable>
     6→#include <cstdint>
     7→#include <cstring>
     8→#include <deque>
     9→#include <fstream>
    10→#include <future>
    11→#include <iostream>
    12→#include <mutex>
    13→#include <set>
    14→#include <string>
    15→#include <thread>
    16→#include <unordered_map>
    17→#include <unordered_set>
    18→#include <vector>
    19→
    20→#include <osmium/handler.hpp>
    21→#include <osmium/io/pbf_input.hpp>
    22→#include <osmium/visitor.hpp>
    23→#include <fcntl.h>
    24→#include <unistd.h>
    25→#include <sys/mman.h>
    26→#include <sys/stat.h>
    27→// FlexMem no longer used — replaced by DenseIndex mmap
    28→
    29→#include <s2/s2cell_id.h>
    30→#include <s2/s2latlng.h>
    31→#include <s2/s2region_coverer.h>
    32→#include <s2/s2polyline.h>
    33→#include <s2/s2polygon.h>
    34→#include <s2/s2loop.h>
    35→#include <s2/s2builder.h>
    36→
    37→// --- Directory creation ---
    38→
    39→static void ensure_dir(const std::string& path) {
    40→    mkdir(path.c_str(), 0755);
    41→}
    42→
    43→// --- Binary format structs ---
    44→
    45→struct WayHeader {
    46→    uint32_t node_offset;
    47→    uint8_t node_count;
    48→    uint32_t name_id;
    49→};
    50→
    51→struct AddrPoint {
    52→    float lat;
    53→    float lng;
    54→    uint32_t housenumber_id;
    55→    uint32_t street_id;
    56→};
    57→
    58→struct InterpWay {
    59→    uint32_t node_offset;
    60→    uint8_t node_count;
    61→    uint32_t street_id;
    62→    uint32_t start_number;
    63→    uint32_t end_number;
    64→    uint8_t interpolation;
    65→};
    66→
    67→struct AdminPolygon {
    68→    uint32_t vertex_offset;
    69→    uint16_t vertex_count;
    70→    uint32_t name_id;
    71→    uint8_t admin_level;
    72→    float area;
    73→    uint16_t country_code;
    74→};
    75→
    76→struct NodeCoord {
    77→    float lat;
    78→    float lng;
    79→};
    80→
    81→static const uint32_t INTERIOR_FLAG = 0x80000000u;
    82→static const uint32_t ID_MASK = 0x7FFFFFFFu;
    83→
    84→// --- Index mode ---
    85→
    86→enum class IndexMode { Full, NoAddresses, AdminOnly };
    87→
    88→// --- String interning ---
    89→
    90→class StringPool {
    91→public:
    92→    uint32_t intern(const std::string& s) {
    93→        auto it = index_.find(s);
    94→        if (it != index_.end()) {
    95→            return it->second;
    96→        }
    97→        uint32_t offset = static_cast<uint32_t>(data_.size());
    98→        index_[s] = offset;
    99→        data_.insert(data_.end(), s.begin(), s.end());
   100→        data_.push_back('\0');

#include <algorithm>
#include <chrono>
#include <atomic>
#include <cmath>
#include <condition_variable>
#include <cstdint>
#include <cstring>
#include <deque>
#include <fstream>
#include <future>
#include <iostream>
#include <mutex>
#include <set>
#include <string>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <vector>

#include <osmium/handler.hpp>
#include <osmium/io/pbf_input.hpp>
#include <osmium/visitor.hpp>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
// FlexMem no longer used — replaced by DenseIndex mmap

#include <s2/s2cell_id.h>
#include <s2/s2latlng.h>
#include <s2/s2region_coverer.h>
#include <s2/s2polyline.h>
#include <s2/s2polygon.h>
#include <s2/s2loop.h>
#include <s2/s2builder.h>

// --- Directory creation ---

static void ensure_dir(const std::string& path) {
    mkdir(path.c_str(), 0755);
}

// --- Binary format structs ---

struct WayHeader {
    uint32_t node_offset;
    uint8_t node_count;
    uint32_t name_id;
};

struct AddrPoint {
    float lat;
    float lng;
    uint32_t housenumber_id;
    uint32_t street_id;
};

struct InterpWay {
    uint32_t node_offset;
    uint8_t node_count;
    uint32_t street_id;
    uint32_t start_number;
    uint32_t end_number;
    uint8_t interpolation;
};

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

struct NodeCoord {
    float lat;
    float lng;
};

static const uint32_t INTERIOR_FLAG = 0x80000000u;
static const uint32_t ID_MASK = 0x7FFFFFFFu;

// --- Index mode ---

enum class IndexMode { Full, NoAddresses, AdminOnly };

// --- String interning ---

class StringPool {
public:
    uint32_t intern(const std::string& s) {
        auto it = index_.find(s);
        if (it != index_.end()) {
            return it->second;
        }
        uint32_t offset = static_cast<uint32_t>(data_.size());
        index_[s] = offset;
        data_.insert(data_.end(), s.begin(), s.end());
        data_.push_back('\0');


Bash

find /root/traccar-geocoder/builder -type f | head -50

List all files in builder directory recursively


Get line count of build_index.cpp


Error: Exit code 1
wc: /root/traccar-geocoder/builder/build_index.cpp: No such file or directory


<tool_use_error>Cancelled: parallel tool call Bash(wc -l /root/traccar-geocoder/builder/bui…) errored</tool_use_error>


Get line count of build_index.cpp in src directory


3033 /root/traccar-geocoder/builder/src/build_index.cpp


     1→cmake_minimum_required(VERSION 3.16)
     2→project(traccar-geocoder-build LANGUAGES CXX)
     3→
     4→set(CMAKE_CXX_STANDARD 17)
     5→set(CMAKE_CXX_STANDARD_REQUIRED ON)
     6→
     7→find_package(Threads REQUIRED)
     8→find_package(ZLIB REQUIRED)
     9→find_package(BZip2 REQUIRED)
    10→find_package(EXPAT REQUIRED)
    11→find_package(s2 QUIET)
    12→
    13→add_executable(build-index src/build_index.cpp)
    14→
    15→if(EXISTS /opt/homebrew/include)
    16→    target_include_directories(build-index PRIVATE /opt/homebrew/include)
    17→endif()
    18→
    19→if(s2_FOUND)
    20→    target_link_libraries(build-index PRIVATE
    21→        s2::s2
    22→        ZLIB::ZLIB
    23→        BZip2::BZip2
    24→        EXPAT::EXPAT
    25→        Threads::Threads
    26→    )
    27→else()
    28→    target_include_directories(build-index PRIVATE /usr/include/s2)
    29→    target_link_libraries(build-index PRIVATE
    30→        s2
    31→        ZLIB::ZLIB
    32→        BZip2::BZip2
    33→        EXPAT::EXPAT
    34→        Threads::Threads
    35→    )
    36→endif()
    37→


List builder directory contents


src/
test/
CMakeLists.txt  852B

1 files, 2 dirs (1.txt)


find /root/traccar-geocoder/builder -type f \( -name "*.h" -o -name "*.hpp" \)

Find all header files in builder


(Bash completed with no output)


ls -lh /root/traccar-geocoder/builder/src/ && ls -lh /root/traccar-geocoder/builder/test/

List contents of src and test directories


total 61K
-rw-r--r-- 1 root root 134K Mar 23 10:17 build_index.cpp
total 44K
-rw-r--r-- 1 root root 3.5K Mar 23 10:37 VALIDATION_REPORT.md
-rw-r--r-- 1 root root  16K Mar 21 13:30 compare_indexes.cpp
-rw-r--r-- 1 root root 5.4K Mar 21 11:08 test_admin_assembly.sh
-rw-r--r-- 1 root root 4.3K Mar 21 11:08 test_admin_compare.sh
-rwxr-xr-x 1 root root 3.3K Mar 23 10:21 test_e2e_queries.sh
-rwxr-xr-x 1 root root 2.8K Mar 23 10:15 test_integration.sh
-rw-r--r-- 1 root root  13K Mar 23 03:48 test_ring_assembly.cpp


Perfect! Here's a comprehensive summary of the traccar-geocoder builder directory structure:

## Directory Structure

**All files in /root/traccar-geocoder/builder/ recursively:**

```
builder/
├── CMakeLists.txt (37 lines)
├── src/
│   └── build_index.cpp (3033 lines)
├── test/
│   ├── VALIDATION_REPORT.md
│   ├── compare_indexes.cpp (16K)
│   ├── test_admin_assembly.sh
│   ├── test_admin_compare.sh
│   ├── test_e2e_queries.sh
│   ├── test_integration.sh
│   └── test_ring_assembly.cpp (13K)
└── build/ (CMake build directory with artifacts and CMakeFiles)
```

## Key Findings

**Line count of build_index.cpp:** 3033 lines

**Header files:** None (no .h or .hpp files exist - the project is a single monolithic cpp file)

**Separate modules:** No separate modules - only:
- Single main source: `src/build_index.cpp`
- Test utilities: `compare_indexes.cpp`, `test_ring_assembly.cpp`
- Test scripts: bash scripts for integration testing

**CMakeLists.txt content:**
- Minimum CMake version: 3.16
- Language: C++17
- Dependencies found via `find_package()`:
  - Threads (REQUIRED)
  - ZLIB (REQUIRED)
  - BZip2 (REQUIRED)
  - EXPAT (REQUIRED)
  - s2 (OPTIONAL - falls back to /usr/include/s2 if not found)
- Single target: `build-index` executable
- Includes `/opt/homebrew/include` if present (macOS support)

The project is structured as a single C++ compilation unit with no header files or modular architecture - all code is in `build_index.cpp`.

agentId: a918ff61ba5b9e3cb (use SendMessage with to: 'a918ff61ba5b9e3cb' to continue this agent)
<usage>total_tokens: 17085
tool_uses: 8
duration_ms: 10721</usage>

Prompt:
Quickly explore the traccar-geocoder builder directory structure. I need to know:
1. All files in /root/traccar-geocoder/builder/ recursively
2. The current line count of build_index.cpp
3. Any existing header files or separate modules
4. The CMakeLists.txt or Makefile content


   100→        data_.push_back('\0');
   101→        return offset;
   102→    }
   103→
   104→    const std::vector<char>& data() const { return data_; }
   105→    std::vector<char>& mutable_data() { return data_; }
   106→
   107→private:
   108→    std::unordered_map<std::string, uint32_t> index_;
   109→    std::vector<char> data_;
   110→};
   111→
   112→// --- Phase timer ---
   113→static void log_phase(const char* name, std::chrono::steady_clock::time_point& t) {
   114→    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
   115→        std::chrono::steady_clock::now() - t).count();
   116→    std::cerr << "  [" << ms/1000 << "." << (ms%1000)/100 << "s] " << name << std::endl;
   117→    t = std::chrono::steady_clock::now();
   118→}
   119→
   120→// --- Deferred S2 work items (computed in parallel after PBF read) ---
   121→
   122→struct DeferredWay {
   123→    uint32_t way_id;
   124→    uint32_t node_offset;
   125→    uint8_t node_count;
   126→};
   127→
   128→struct DeferredInterp {
   129→    uint32_t interp_id;
   130→    uint32_t node_offset;
   131→    uint8_t node_count;
   132→};
   133→
   134→// Collected relation data for parallel admin assembly
   135→struct CollectedRelation {
   136→    int64_t id;
   137→    uint8_t admin_level;
   138→    std::string name;
   139→    std::string country_code;
   140→    bool is_postal;
   141→    std::vector<std::pair<int64_t, std::string>> members; // (way_id, role)
   142→};
   143→
   144→// Forward declaration
   145→static std::vector<std::pair<S2CellId, bool>> cover_polygon(const std::vector<std::pair<double,double>>& vertices);
   146→
   147→// --- Thread pool for concurrent admin polygon S2 covering ---
   148→
   149→class AdminCoverPool {
   150→public:
   151→    struct WorkItem {
   152→        uint32_t poly_id;
   153→        std::vector<std::pair<double,double>> vertices;
   154→    };
   155→
   156→    explicit AdminCoverPool(size_t num_threads) : stop_(false) {
   157→        for (size_t i = 0; i < num_threads; i++) {
   158→            workers_.emplace_back([this]() { worker_loop(); });
   159→        }
   160→    }
   161→
   162→    ~AdminCoverPool() {
   163→        {
   164→            std::lock_guard<std::mutex> lock(mutex_);
   165→            stop_ = true;
   166→        }
   167→        cv_.notify_all();
   168→        for (auto& t : workers_) t.join();
   169→    }
   170→
   171→    void submit(uint32_t poly_id, std::vector<std::pair<double,double>>&& vertices) {
   172→        {
   173→            std::lock_guard<std::mutex> lock(mutex_);
   174→            queue_.push_back({poly_id, std::move(vertices)});
   175→        }
   176→        cv_.notify_one();
   177→    }
   178→
   179→    // Wait for all pending work to complete, then return merged results
   180→    std::unordered_map<uint64_t, std::vector<uint32_t>> drain() {
   181→        // Wait until queue is empty and all workers are idle
   182→        {
   183→            std::unique_lock<std::mutex> lock(mutex_);
   184→            done_cv_.wait(lock, [this]() { return queue_.empty() && active_workers_ == 0; });
   185→        }
   186→
   187→        // Merge all thread-local results
   188→        std::unordered_map<uint64_t, std::vector<uint32_t>> merged;
   189→        for (auto& local : thread_results_) {
   190→            for (auto& [cell_id, ids] : local) {
   191→                auto& target = merged[cell_id];
   192→                target.insert(target.end(), ids.begin(), ids.end());
   193→            }
   194→        }
   195→        return merged;
   196→    }
   197→
   198→private:
   199→    void worker_loop() {
   200→        size_t my_idx;
   201→        {
   202→            std::lock_guard<std::mutex> lock(mutex_);
   203→            my_idx = thread_results_.size();
   204→            thread_results_.emplace_back();
   205→        }
   206→
   207→        while (true) {
   208→            WorkItem item;
   209→            {
   210→                std::unique_lock<std::mutex> lock(mutex_);
   211→                cv_.wait(lock, [this]() { return stop_ || !queue_.empty(); });
   212→                if (stop_ && queue_.empty()) return;
   213→                item = std::move(queue_.front());
   214→                queue_.pop_front();
   215→                active_workers_++;
   216→            }
   217→
   218→            // Compute S2 covering (no locks needed — pure computation)
   219→            auto cell_ids = cover_polygon(item.vertices);
   220→            auto& local = thread_results_[my_idx];
   221→            for (const auto& [cell_id, is_interior] : cell_ids) {
   222→                uint32_t entry = is_interior ? (item.poly_id | INTERIOR_FLAG) : item.poly_id;
   223→                local[cell_id.id()].push_back(entry);
   224→            }
   225→
   226→            {
   227→                std::lock_guard<std::mutex> lock(mutex_);
   228→                active_workers_--;
   229→            }
   230→            done_cv_.notify_all();
   231→        }
   232→    }
   233→
   234→    std::vector<std::thread> workers_;
   235→    std::deque<WorkItem> queue_;
   236→    std::vector<std::unordered_map<uint64_t, std::vector<uint32_t>>> thread_results_;
   237→    std::mutex mutex_;
   238→    std::condition_variable cv_;
   239→    std::condition_variable done_cv_;
   240→    size_t active_workers_ = 0;
   241→    bool stop_;
   242→};
   243→
   244→// --- Parsed data container ---
   245→
   246→// Sorted cell-item pair for direct entry writing (avoids hash map)
   247→struct CellItemPair { uint64_t cell_id; uint32_t item_id; };
   248→
   249→struct ParsedData {
   250→    StringPool string_pool;
   251→    std::vector<WayHeader> ways;
   252→    std::vector<NodeCoord> street_nodes;
   253→    std::unordered_map<uint64_t, std::vector<uint32_t>> cell_to_ways;
   254→    std::vector<AddrPoint> addr_points;
   255→    std::unordered_map<uint64_t, std::vector<uint32_t>> cell_to_addrs;
   256→    std::vector<InterpWay> interp_ways;
   257→    std::vector<NodeCoord> interp_nodes;
   258→    std::unordered_map<uint64_t, std::vector<uint32_t>> cell_to_interps;
   259→    std::vector<AdminPolygon> admin_polygons;
   260→    std::vector<NodeCoord> admin_vertices;
   261→    std::unordered_map<uint64_t, std::vector<uint32_t>> cell_to_admin;
   262→
   263→    // Deferred work for parallel S2 computation (ways + interps)
   264→    std::vector<DeferredWay> deferred_ways;
   265→    std::vector<DeferredInterp> deferred_interps;
   266→
   267→    // Sorted (cell_id, item_id) pairs — kept for direct entry writing
   268→    std::vector<CellItemPair> sorted_way_cells;
   269→    std::vector<CellItemPair> sorted_addr_cells;
   270→
   271→    // Collected data for parallel admin assembly
   272→    std::vector<CollectedRelation> collected_relations;
   273→    struct WayGeometry {
   274→        std::vector<std::pair<double,double>> coords;
   275→        int64_t first_node_id;
   276→        int64_t last_node_id;
   277→    };
   278→    std::unordered_map<int64_t, WayGeometry> way_geometries;
   279→};
   280→
   281→// --- S2 helpers ---
   282→
   283→static int kStreetCellLevel = 17;
   284→static int kAdminCellLevel = 10;
   285→static int kMaxAdminLevel = 0;  // 0 means no filtering
   286→
   287→// Fast cover_edge: compute cells covered by a line segment.
   288→// Fast path for short edges (same cell or adjacent) avoids S2RegionCoverer entirely.
   289→// ~80% of street edges are within 1-2 cells, so this eliminates most S2 library calls.
   290→static void cover_edge(double lat1, double lng1, double lat2, double lng2,
   291→                        std::vector<S2CellId>& out) {
   292→    out.clear();
   293→
   294→    // Fast: compute cell IDs directly from coordinates
   295→    S2CellId c1 = S2CellId(S2LatLng::FromDegrees(lat1, lng1)).parent(kStreetCellLevel);
   296→    S2CellId c2 = S2CellId(S2LatLng::FromDegrees(lat2, lng2)).parent(kStreetCellLevel);
   297→
   298→    // Same cell — most common case for short edges
   299→    if (c1 == c2) {

        data_.push_back('\0');
        return offset;
    }

    const std::vector<char>& data() const { return data_; }
    std::vector<char>& mutable_data() { return data_; }

private:
    std::unordered_map<std::string, uint32_t> index_;
    std::vector<char> data_;
};

// --- Phase timer ---
static void log_phase(const char* name, std::chrono::steady_clock::time_point& t) {
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
        std::chrono::steady_clock::now() - t).count();
    std::cerr << "  [" << ms/1000 << "." << (ms%1000)/100 << "s] " << name << std::endl;
    t = std::chrono::steady_clock::now();
}

// --- Deferred S2 work items (computed in parallel after PBF read) ---

struct DeferredWay {
    uint32_t way_id;
    uint32_t node_offset;
    uint8_t node_count;
};

struct DeferredInterp {
    uint32_t interp_id;
    uint32_t node_offset;
    uint8_t node_count;
};

// Collected relation data for parallel admin assembly
struct CollectedRelation {
    int64_t id;
    uint8_t admin_level;
    std::string name;
    std::string country_code;
    bool is_postal;
    std::vector<std::pair<int64_t, std::string>> members; // (way_id, role)
};

// Forward declaration
static std::vector<std::pair<S2CellId, bool>> cover_polygon(const std::vector<std::pair<double,double>>& vertices);

// --- Thread pool for concurrent admin polygon S2 covering ---

class AdminCoverPool {
public:
    struct WorkItem {
        uint32_t poly_id;
        std::vector<std::pair<double,double>> vertices;
    };

    explicit AdminCoverPool(size_t num_threads) : stop_(false) {
        for (size_t i = 0; i < num_threads; i++) {
            workers_.emplace_back([this]() { worker_loop(); });
        }
    }

    ~AdminCoverPool() {
        {
            std::lock_guard<std::mutex> lock(mutex_);
            stop_ = true;
        }
        cv_.notify_all();
        for (auto& t : workers_) t.join();
    }

    void submit(uint32_t poly_id, std::vector<std::pair<double,double>>&& vertices) {
        {
            std::lock_guard<std::mutex> lock(mutex_);
            queue_.push_back({poly_id, std::move(vertices)});
        }
        cv_.notify_one();
    }

    // Wait for all pending work to complete, then return merged results
    std::unordered_map<uint64_t, std::vector<uint32_t>> drain() {
        // Wait until queue is empty and all workers are idle
        {
            std::unique_lock<std::mutex> lock(mutex_);
            done_cv_.wait(lock, [this]() { return queue_.empty() && active_workers_ == 0; });
        }

        // Merge all thread-local results
        std::unordered_map<uint64_t, std::vector<uint32_t>> merged;
        for (auto& local : thread_results_) {
            for (auto& [cell_id, ids] : local) {
                auto& target = merged[cell_id];
                target.insert(target.end(), ids.begin(), ids.end());
            }
        }
        return merged;
    }

private:
    void worker_loop() {
        size_t my_idx;
        {
            std::lock_guard<std::mutex> lock(mutex_);
            my_idx = thread_results_.size();
            thread_results_.emplace_back();
        }

        while (true) {
            WorkItem item;
            {
                std::unique_lock<std::mutex> lock(mutex_);
                cv_.wait(lock, [this]() { return stop_ || !queue_.empty(); });
                if (stop_ && queue_.empty()) return;
                item = std::move(queue_.front());
                queue_.pop_front();
                active_workers_++;
            }

            // Compute S2 covering (no locks needed — pure computation)
            auto cell_ids = cover_polygon(item.vertices);
            auto& local = thread_results_[my_idx];
            for (const auto& [cell_id, is_interior] : cell_ids) {
                uint32_t entry = is_interior ? (item.poly_id | INTERIOR_FLAG) : item.poly_id;
                local[cell_id.id()].push_back(entry);
            }

            {
                std::lock_guard<std::mutex> lock(mutex_);
                active_workers_--;
            }
            done_cv_.notify_all();
        }
    }

    std::vector<std::thread> workers_;
    std::deque<WorkItem> queue_;
    std::vector<std::unordered_map<uint64_t, std::vector<uint32_t>>> thread_results_;
    std::mutex mutex_;
    std::condition_variable cv_;
    std::condition_variable done_cv_;
    size_t active_workers_ = 0;
    bool stop_;
};

// --- Parsed data container ---

// Sorted cell-item pair for direct entry writing (avoids hash map)
struct CellItemPair { uint64_t cell_id; uint32_t item_id; };

struct ParsedData {
    StringPool string_pool;
    std::vector<WayHeader> ways;
    std::vector<NodeCoord> street_nodes;
    std::unordered_map<uint64_t, std::vector<uint32_t>> cell_to_ways;
    std::vector<AddrPoint> addr_points;
    std::unordered_map<uint64_t, std::vector<uint32_t>> cell_to_addrs;
    std::vector<InterpWay> interp_ways;
    std::vector<NodeCoord> interp_nodes;
    std::unordered_map<uint64_t, std::vector<uint32_t>> cell_to_interps;
    std::vector<AdminPolygon> admin_polygons;
    std::vector<NodeCoord> admin_vertices;
    std::unordered_map<uint64_t, std::vector<uint32_t>> cell_to_admin;

    // Deferred work for parallel S2 computation (ways + interps)
    std::vector<DeferredWay> deferred_ways;
    std::vector<DeferredInterp> deferred_interps;

    // Sorted (cell_id, item_id) pairs — kept for direct entry writing
    std::vector<CellItemPair> sorted_way_cells;
    std::vector<CellItemPair> sorted_addr_cells;

    // Collected data for parallel admin assembly
    std::vector<CollectedRelation> collected_relations;
    struct WayGeometry {
        std::vector<std::pair<double,double>> coords;
        int64_t first_node_id;
        int64_t last_node_id;
    };
    std::unordered_map<int64_t, WayGeometry> way_geometries;
};

// --- S2 helpers ---

static int kStreetCellLevel = 17;
static int kAdminCellLevel = 10;
static int kMaxAdminLevel = 0;  // 0 means no filtering

// Fast cover_edge: compute cells covered by a line segment.
// Fast path for short edges (same cell or adjacent) avoids S2RegionCoverer entirely.
// ~80% of street edges are within 1-2 cells, so this eliminates most S2 library calls.
static void cover_edge(double lat1, double lng1, double lat2, double lng2,
                        std::vector<S2CellId>& out) {
    out.clear();

    // Fast: compute cell IDs directly from coordinates
    S2CellId c1 = S2CellId(S2LatLng::FromDegrees(lat1, lng1)).parent(kStreetCellLevel);
    S2CellId c2 = S2CellId(S2LatLng::FromDegrees(lat2, lng2)).parent(kStreetCellLevel);

    // Same cell — most common case for short edges
    if (c1 == c2) {


   300→        out.push_back(c1);
   301→        return;
   302→    }
   303→
   304→    // Adjacent cells — check midpoint
   305→    S2CellId cm = S2CellId(S2LatLng::FromDegrees((lat1+lat2)/2, (lng1+lng2)/2)).parent(kStreetCellLevel);
   306→    if (cm == c1 || cm == c2) {
   307→        out.push_back(c1);
   308→        out.push_back(c2);
   309→        return;
   310→    }
   311→
   312→    // Longer edge — use S2RegionCoverer for correctness (handles geodesic curves)
   313→    thread_local S2RegionCoverer coverer = []() {
   314→        S2RegionCoverer::Options options;
   315→        options.set_fixed_level(kStreetCellLevel);
   316→        return S2RegionCoverer(options);
   317→    }();
   318→
   319→    S2Point p1 = S2LatLng::FromDegrees(lat1, lng1).ToPoint();
   320→    S2Point p2 = S2LatLng::FromDegrees(lat2, lng2).ToPoint();
   321→    if (p1 == p2) { out.push_back(c1); return; }
   322→
   323→    std::vector<S2Point> points = {p1, p2};
   324→    S2Polyline polyline(points);
   325→    S2CellUnion covering = coverer.GetCovering(polyline);
   326→    out = std::move(covering.cell_ids());
   327→}
   328→
   329→// Legacy overload for code that still uses return-by-value
   330→static std::vector<S2CellId> cover_edge(double lat1, double lng1, double lat2, double lng2) {
   331→    std::vector<S2CellId> result;
   332→    cover_edge(lat1, lng1, lat2, lng2, result);
   333→    return result;
   334→}
   335→
   336→static S2CellId point_to_cell(double lat, double lng) {
   337→    return S2CellId(S2LatLng::FromDegrees(lat, lng)).parent(kStreetCellLevel);
   338→}
   339→
   340→// Approximate polygon area in square degrees
   341→static float polygon_area(const std::vector<std::pair<double,double>>& vertices) {
   342→    double area = 0;
   343→    size_t n = vertices.size();
   344→    for (size_t i = 0; i < n; i++) {
   345→        size_t j = (i + 1) % n;
   346→        area += vertices[i].first * vertices[j].second;
   347→        area -= vertices[j].first * vertices[i].second;
   348→    }
   349→    return static_cast<float>(std::fabs(area) / 2.0);
   350→}
   351→
   352→// Returns pairs of (cell_id, is_interior)
   353→static std::vector<std::pair<S2CellId, bool>> cover_polygon(const std::vector<std::pair<double,double>>& vertices) {
   354→    std::vector<S2Point> points;
   355→    points.reserve(vertices.size());
   356→    for (const auto& [lat, lng] : vertices) {
   357→        S2Point p = S2LatLng::FromDegrees(lat, lng).ToPoint();
   358→        if (!points.empty() && points.back() == p) continue;
   359→        points.push_back(p);
   360→    }
   361→    // Remove closing duplicate if first == last
   362→    if (points.size() > 1 && points.front() == points.back()) {
   363→        points.pop_back();
   364→    }
   365→    if (points.size() < 3) return {};
   366→
   367→    // Build S2Loop (must be CCW), skip invalid polygons
   368→    S2Error error;
   369→    auto loop = std::make_unique<S2Loop>(points, S2Debug::DISABLE);
   370→    loop->Normalize();
   371→    if (loop->FindValidationError(&error)) return {};
   372→
   373→    S2Polygon polygon(std::move(loop));
   374→
   375→    S2RegionCoverer::Options options;
   376→    options.set_max_level(kAdminCellLevel);
   377→    options.set_max_cells(200);
   378→
   379→    S2RegionCoverer coverer(options);
   380→    S2CellUnion covering = coverer.GetCovering(polygon);
   381→    S2CellUnion interior = coverer.GetInteriorCovering(polygon);
   382→
   383→    // Build set of interior cell IDs for fast lookup
   384→    std::unordered_set<uint64_t> interior_set;
   385→    for (const auto& cell : interior.cell_ids()) {
   386→        if (cell.level() <= kAdminCellLevel) {
   387→            auto begin = cell.range_min().parent(kAdminCellLevel);
   388→            auto end = cell.range_max().parent(kAdminCellLevel);
   389→            for (auto c = begin; c != end; c = c.next()) {
   390→                interior_set.insert(c.id());
   391→            }
   392→            interior_set.insert(end.id());
   393→        } else {
   394→            interior_set.insert(cell.parent(kAdminCellLevel).id());
   395→        }
   396→    }
   397→
   398→    // Normalize all covering cells to kAdminCellLevel
   399→    std::vector<std::pair<S2CellId, bool>> result;
   400→    for (const auto& cell : covering.cell_ids()) {
   401→        if (cell.level() <= kAdminCellLevel) {
   402→            auto begin = cell.range_min().parent(kAdminCellLevel);
   403→            auto end = cell.range_max().parent(kAdminCellLevel);
   404→            for (auto c = begin; c != end; c = c.next()) {
   405→                result.emplace_back(c, interior_set.count(c.id()) > 0);
   406→            }
   407→            result.emplace_back(end, interior_set.count(end.id()) > 0);
   408→        } else {
   409→            auto parent = cell.parent(kAdminCellLevel);
   410→            result.emplace_back(parent, interior_set.count(parent.id()) > 0);
   411→        }
   412→    }
   413→
   414→    // Deduplicate by cell_id, keeping interior=true if any duplicate is interior
   415→    std::sort(result.begin(), result.end(), [](const auto& a, const auto& b) {
   416→        return a.first < b.first;
   417→    });
   418→    auto it = result.begin();
   419→    for (auto curr = result.begin(); curr != result.end(); ) {
   420→        auto next = curr + 1;
   421→        bool is_interior = curr->second;
   422→        while (next != result.end() && next->first == curr->first) {
   423→            is_interior = is_interior || next->second;
   424→            ++next;
   425→        }
   426→        *it = {curr->first, is_interior};
   427→        ++it;
   428→        curr = next;
   429→    }
   430→    result.erase(it, result.end());
   431→    return result;
   432→}
   433→
   434→// --- Douglas-Peucker simplification ---
   435→
   436→static void dp_simplify(const std::vector<std::pair<double,double>>& pts,
   437→                        size_t start, size_t end, double epsilon,
   438→                        std::vector<bool>& keep) {
   439→    if (end <= start + 1) return;
   440→
   441→    double max_dist = 0;
   442→    size_t max_idx = start;
   443→
   444→    double ax = pts[start].first, ay = pts[start].second;
   445→    double bx = pts[end].first, by = pts[end].second;
   446→    double dx = bx - ax, dy = by - ay;
   447→    double len_sq = dx * dx + dy * dy;
   448→
   449→    for (size_t i = start + 1; i < end; i++) {
   450→        double px = pts[i].first - ax, py = pts[i].second - ay;
   451→        double dist;
   452→        if (len_sq == 0) {
   453→            dist = std::sqrt(px * px + py * py);
   454→        } else {
   455→            double t = std::max(0.0, std::min(1.0, (px * dx + py * dy) / len_sq));
   456→            double proj_x = t * dx - px, proj_y = t * dy - py;
   457→            dist = std::sqrt(proj_x * proj_x + proj_y * proj_y);
   458→        }
   459→        if (dist > max_dist) {
   460→            max_dist = dist;
   461→            max_idx = i;
   462→        }
   463→    }
   464→
   465→    if (max_dist > epsilon) {
   466→        keep[max_idx] = true;
   467→        dp_simplify(pts, start, max_idx, epsilon, keep);
   468→        dp_simplify(pts, max_idx, end, epsilon, keep);
   469→    }
   470→}
   471→
   472→static std::vector<std::pair<double,double>> simplify_polygon(
   473→    const std::vector<std::pair<double,double>>& pts, size_t max_vertices) {
   474→    if (pts.size() <= max_vertices) return pts;
   475→
   476→    // Binary search for epsilon that gives ~max_vertices
   477→    double lo = 0, hi = 1.0;
   478→    std::vector<std::pair<double,double>> result;
   479→
   480→    for (int iter = 0; iter < 20; iter++) {
   481→        double epsilon = (lo + hi) / 2;
   482→        std::vector<bool> keep(pts.size(), false);
   483→        keep[0] = true;
   484→        keep[pts.size() - 1] = true;
   485→        dp_simplify(pts, 0, pts.size() - 1, epsilon, keep);
   486→
   487→        size_t count = 0;
   488→        for (bool k : keep) if (k) count++;
   489→
   490→        if (count > max_vertices) {
   491→            lo = epsilon;
   492→        } else {
   493→            hi = epsilon;
   494→        }
   495→    }
   496→
   497→    std::vector<bool> keep(pts.size(), false);
   498→    keep[0] = true;
   499→    keep[pts.size() - 1] = true;

        out.push_back(c1);
        return;
    }

    // Adjacent cells — check midpoint
    S2CellId cm = S2CellId(S2LatLng::FromDegrees((lat1+lat2)/2, (lng1+lng2)/2)).parent(kStreetCellLevel);
    if (cm == c1 || cm == c2) {
        out.push_back(c1);
        out.push_back(c2);
        return;
    }

    // Longer edge — use S2RegionCoverer for correctness (handles geodesic curves)
    thread_local S2RegionCoverer coverer = []() {
        S2RegionCoverer::Options options;
        options.set_fixed_level(kStreetCellLevel);
        return S2RegionCoverer(options);
    }();

    S2Point p1 = S2LatLng::FromDegrees(lat1, lng1).ToPoint();
    S2Point p2 = S2LatLng::FromDegrees(lat2, lng2).ToPoint();
    if (p1 == p2) { out.push_back(c1); return; }

    std::vector<S2Point> points = {p1, p2};
    S2Polyline polyline(points);
    S2CellUnion covering = coverer.GetCovering(polyline);
    out = std::move(covering.cell_ids());
}

// Legacy overload for code that still uses return-by-value
static std::vector<S2CellId> cover_edge(double lat1, double lng1, double lat2, double lng2) {
    std::vector<S2CellId> result;
    cover_edge(lat1, lng1, lat2, lng2, result);
    return result;
}

static S2CellId point_to_cell(double lat, double lng) {
    return S2CellId(S2LatLng::FromDegrees(lat, lng)).parent(kStreetCellLevel);
}

// Approximate polygon area in square degrees
static float polygon_area(const std::vector<std::pair<double,double>>& vertices) {
    double area = 0;
    size_t n = vertices.size();
    for (size_t i = 0; i < n; i++) {
        size_t j = (i + 1) % n;
        area += vertices[i].first * vertices[j].second;
        area -= vertices[j].first * vertices[i].second;
    }
    return static_cast<float>(std::fabs(area) / 2.0);
}

// Returns pairs of (cell_id, is_interior)
static std::vector<std::pair<S2CellId, bool>> cover_polygon(const std::vector<std::pair<double,double>>& vertices) {
    std::vector<S2Point> points;
    points.reserve(vertices.size());
    for (const auto& [lat, lng] : vertices) {
        S2Point p = S2LatLng::FromDegrees(lat, lng).ToPoint();
        if (!points.empty() && points.back() == p) continue;
        points.push_back(p);
    }
    // Remove closing duplicate if first == last
    if (points.size() > 1 && points.front() == points.back()) {
        points.pop_back();
    }
    if (points.size() < 3) return {};

    // Build S2Loop (must be CCW), skip invalid polygons
    S2Error error;
    auto loop = std::make_unique<S2Loop>(points, S2Debug::DISABLE);
    loop->Normalize();
    if (loop->FindValidationError(&error)) return {};

    S2Polygon polygon(std::move(loop));

    S2RegionCoverer::Options options;
    options.set_max_level(kAdminCellLevel);
    options.set_max_cells(200);

    S2RegionCoverer coverer(options);
    S2CellUnion covering = coverer.GetCovering(polygon);
    S2CellUnion interior = coverer.GetInteriorCovering(polygon);

    // Build set of interior cell IDs for fast lookup
    std::unordered_set<uint64_t> interior_set;
    for (const auto& cell : interior.cell_ids()) {
        if (cell.level() <= kAdminCellLevel) {
            auto begin = cell.range_min().parent(kAdminCellLevel);
            auto end = cell.range_max().parent(kAdminCellLevel);
            for (auto c = begin; c != end; c = c.next()) {
                interior_set.insert(c.id());
            }
            interior_set.insert(end.id());
        } else {
            interior_set.insert(cell.parent(kAdminCellLevel).id());
        }
    }

    // Normalize all covering cells to kAdminCellLevel
    std::vector<std::pair<S2CellId, bool>> result;
    for (const auto& cell : covering.cell_ids()) {
        if (cell.level() <= kAdminCellLevel) {
            auto begin = cell.range_min().parent(kAdminCellLevel);
            auto end = cell.range_max().parent(kAdminCellLevel);
            for (auto c = begin; c != end; c = c.next()) {
                result.emplace_back(c, interior_set.count(c.id()) > 0);
            }
            result.emplace_back(end, interior_set.count(end.id()) > 0);
        } else {
            auto parent = cell.parent(kAdminCellLevel);
            result.emplace_back(parent, interior_set.count(parent.id()) > 0);
        }
    }

    // Deduplicate by cell_id, keeping interior=true if any duplicate is interior
    std::sort(result.begin(), result.end(), [](const auto& a, const auto& b) {
        return a.first < b.first;
    });
    auto it = result.begin();
    for (auto curr = result.begin(); curr != result.end(); ) {
        auto next = curr + 1;
        bool is_interior = curr->second;
        while (next != result.end() && next->first == curr->first) {
            is_interior = is_interior || next->second;
            ++next;
        }
        *it = {curr->first, is_interior};
        ++it;
        curr = next;
    }
    result.erase(it, result.end());
    return result;
}

// --- Douglas-Peucker simplification ---

static void dp_simplify(const std::vector<std::pair<double,double>>& pts,
                        size_t start, size_t end, double epsilon,
                        std::vector<bool>& keep) {
    if (end <= start + 1) return;

    double max_dist = 0;
    size_t max_idx = start;

    double ax = pts[start].first, ay = pts[start].second;
    double bx = pts[end].first, by = pts[end].second;
    double dx = bx - ax, dy = by - ay;
    double len_sq = dx * dx + dy * dy;

    for (size_t i = start + 1; i < end; i++) {
        double px = pts[i].first - ax, py = pts[i].second - ay;
        double dist;
        if (len_sq == 0) {
            dist = std::sqrt(px * px + py * py);
        } else {
            double t = std::max(0.0, std::min(1.0, (px * dx + py * dy) / len_sq));
            double proj_x = t * dx - px, proj_y = t * dy - py;
            dist = std::sqrt(proj_x * proj_x + proj_y * proj_y);
        }
        if (dist > max_dist) {
            max_dist = dist;
            max_idx = i;
        }
    }

    if (max_dist > epsilon) {
        keep[max_idx] = true;
        dp_simplify(pts, start, max_idx, epsilon, keep);
        dp_simplify(pts, max_idx, end, epsilon, keep);
    }
}

static std::vector<std::pair<double,double>> simplify_polygon(
    const std::vector<std::pair<double,double>>& pts, size_t max_vertices) {
    if (pts.size() <= max_vertices) return pts;

    // Binary search for epsilon that gives ~max_vertices
    double lo = 0, hi = 1.0;
    std::vector<std::pair<double,double>> result;

    for (int iter = 0; iter < 20; iter++) {
        double epsilon = (lo + hi) / 2;
        std::vector<bool> keep(pts.size(), false);
        keep[0] = true;
        keep[pts.size() - 1] = true;
        dp_simplify(pts, 0, pts.size() - 1, epsilon, keep);

        size_t count = 0;
        for (bool k : keep) if (k) count++;

        if (count > max_vertices) {
            lo = epsilon;
        } else {
            hi = epsilon;
        }
    }

    std::vector<bool> keep(pts.size(), false);
    keep[0] = true;
    keep[pts.size() - 1] = true;


   500→    dp_simplify(pts, 0, pts.size() - 1, hi, keep);
   501→
   502→    result.clear();
   503→    for (size_t i = 0; i < pts.size(); i++) {
   504→        if (keep[i]) result.push_back(pts[i]);
   505→    }
   506→    return result;
   507→}
   508→
   509→// --- Ring self-intersection check (matches osmium's segment crossing detection) ---
   510→
   511→static bool segments_intersect(double ax1, double ay1, double ax2, double ay2,
   512→                                double bx1, double by1, double bx2, double by2) {
   513→    // Check if segment (a1,a2) crosses segment (b1,b2)
   514→    // Using cross product orientation test
   515→    auto cross = [](double ox, double oy, double ax, double ay, double bx, double by) -> double {
   516→        return (ax - ox) * (by - oy) - (ay - oy) * (bx - ox);
   517→    };
   518→    double d1 = cross(bx1, by1, bx2, by2, ax1, ay1);
   519→    double d2 = cross(bx1, by1, bx2, by2, ax2, ay2);
   520→    double d3 = cross(ax1, ay1, ax2, ay2, bx1, by1);
   521→    double d4 = cross(ax1, ay1, ax2, ay2, bx2, by2);
   522→    if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
   523→        ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
   524→        return true;
   525→    }
   526→    return false;
   527→}
   528→
   529→static bool ring_has_self_intersection(const std::vector<std::pair<double,double>>& ring) {
   530→    // Sweep-line inspired check: sort segments by min-x, then only check
   531→    // overlapping x-ranges. Much faster than O(n²) for large rings.
   532→    size_t n = ring.size();
   533→    if (n < 4) return false;
   534→
   535→    // For small rings, brute force is fine
   536→    if (n <= 32) {
   537→        for (size_t i = 0; i + 1 < n; i++) {
   538→            for (size_t j = i + 2; j + 1 < n; j++) {
   539→                if (j == i + 1 || (i == 0 && j == n - 2)) continue;
   540→                if (segments_intersect(ring[i].first, ring[i].second,
   541→                                        ring[i+1].first, ring[i+1].second,
   542→                                        ring[j].first, ring[j].second,
   543→                                        ring[j+1].first, ring[j+1].second))
   544→                    return true;
   545→            }
   546→        }
   547→        return false;
   548→    }
   549→
   550→    // For larger rings, sort segments by min-x and prune non-overlapping
   551→    struct Seg { double min_x, max_x; size_t idx; };
   552→    std::vector<Seg> segs(n - 1);
   553→    for (size_t i = 0; i + 1 < n; i++) {
   554→        double x1 = ring[i].second, x2 = ring[i+1].second; // use lng as x
   555→        segs[i] = {std::min(x1,x2), std::max(x1,x2), i};
   556→    }
   557→    std::sort(segs.begin(), segs.end(), [](const Seg& a, const Seg& b) {
   558→        return a.min_x < b.min_x;
   559→    });
   560→
   561→    for (size_t a = 0; a + 1 < segs.size(); a++) {
   562→        for (size_t b = a + 1; b < segs.size(); b++) {
   563→            if (segs[b].min_x > segs[a].max_x) break; // no more x-overlap
   564→            size_t i = segs[a].idx, j = segs[b].idx;
   565→            // Skip adjacent segments
   566→            if (j == i + 1 || i == j + 1) continue;
   567→            if ((i == 0 && j == n - 2) || (j == 0 && i == n - 2)) continue;
   568→            if (segments_intersect(ring[i].first, ring[i].second,
   569→                                    ring[i+1].first, ring[i+1].second,
   570→                                    ring[j].first, ring[j].second,
   571→                                    ring[j+1].first, ring[j+1].second))
   572→                return true;
   573→        }
   574→    }
   575→    return false;
   576→}
   577→
   578→// Check if a ring has duplicate coordinates (spikes/figure-8 from merged holes)
   579→// A valid simple polygon visits each coordinate exactly once (except closing point).
   580→static bool ring_has_duplicate_coords(const std::vector<std::pair<double,double>>& ring,
   581→                                       decltype(std::function<int64_t(double,double)>()) coord_key_fn) {
   582→    std::unordered_set<int64_t> seen;
   583→    for (size_t i = 0; i + 1 < ring.size(); i++) { // skip closing point
   584→        int64_t k = coord_key_fn(ring[i].first, ring[i].second);
   585→        if (!seen.insert(k).second) return true; // duplicate
   586→    }
   587→    return false;
   588→}
   589→
   590→// --- Parallel admin: assemble outer rings from collected way geometries ---
   591→
   592→static std::vector<std::vector<std::pair<double,double>>> assemble_outer_rings(
   593→    const std::vector<std::pair<int64_t, std::string>>& members,
   594→    const std::unordered_map<int64_t, ParsedData::WayGeometry>& way_geoms,
   595→    bool include_all_roles = false)
   596→{
   597→    // Coordinate-based ring assembly: split ways at shared internal nodes,
   598→    // then stitch sub-ways at endpoints with backtracking.
   599→    // This matches osmium's segment-level approach.
   600→
   601→    // Coordinate key matching osmium's Location (int32_t nanodegrees)
   602→    auto coord_key = [](double lat, double lng) -> int64_t {
   603→        int32_t ilat = static_cast<int32_t>(lat * 1e7 + (lat >= 0 ? 0.5 : -0.5));
   604→        int32_t ilng = static_cast<int32_t>(lng * 1e7 + (lng >= 0 ? 0.5 : -0.5));
   605→        return (static_cast<int64_t>(ilat) << 32) | static_cast<uint32_t>(ilng);
   606→    };
   607→
   608→    // Collect coordinates from member ways.
   609→    // First pass uses outer/empty-role ways only. If that produces 0 rings,
   610→    // retry with ALL ways (matching osmium's check_roles=false fallback) —
   611→    // inner ways sometimes bridge gaps between outer ways.
   612→    std::unordered_map<int64_t, int> coord_count;
   613→    std::vector<const std::vector<std::pair<double,double>>*> way_coords;
   614→    for (const auto& [way_id, role] : members) {
   615→        if (!include_all_roles && role != "outer" && !role.empty()) continue;
   616→        auto it = way_geoms.find(way_id);
   617→        if (it == way_geoms.end() || it->second.coords.empty()) continue;
   618→        way_coords.push_back(&it->second.coords);
   619→        for (const auto& [lat, lng] : it->second.coords) {
   620→            coord_count[coord_key(lat, lng)]++;
   621→        }
   622→    }
   623→
   624→    // A coordinate is a "split point" if it appears in 2+ ways' node lists.
   625→    // Endpoints always count, internal nodes that appear in multiple ways need splitting.
   626→    // Also, all way endpoints are natural split points.
   627→    std::unordered_set<int64_t> split_points;
   628→    for (auto* coords : way_coords) {
   629→        split_points.insert(coord_key(coords->front().first, coords->front().second));
   630→        split_points.insert(coord_key(coords->back().first, coords->back().second));
   631→    }
   632→    for (auto& [ck, cnt] : coord_count) {
   633→        if (cnt > 1) split_points.insert(ck);
   634→    }
   635→
   636→    // Split each way at split points to create sub-ways
   637→    struct SubWay {
   638→        std::vector<std::pair<double,double>> coords;
   639→    };
   640→    std::vector<SubWay> sub_ways;
   641→    for (auto* coords : way_coords) {
   642→        size_t seg_start = 0;
   643→        for (size_t i = 1; i < coords->size(); i++) {
   644→            int64_t ck = coord_key((*coords)[i].first, (*coords)[i].second);
   645→            if (split_points.count(ck) && i > seg_start) {
   646→                // Create sub-way from seg_start to i (inclusive)
   647→                SubWay sw;
   648→                sw.coords.assign(coords->begin() + seg_start, coords->begin() + i + 1);
   649→                if (sw.coords.size() >= 2) {
   650→                    sub_ways.push_back(std::move(sw));
   651→                }
   652→                seg_start = i;
   653→            }
   654→        }
   655→        // Handle remaining segment (if not already added)
   656→        if (seg_start < coords->size() - 1) {
   657→            SubWay sw;
   658→            sw.coords.assign(coords->begin() + seg_start, coords->end());
   659→            if (sw.coords.size() >= 2) {
   660→                sub_ways.push_back(std::move(sw));
   661→            }
   662→        }
   663→    }
   664→
   665→    // Build adjacency by sub-way coordinate endpoints
   666→    struct WayRef {
   667→        int64_t way_id;
   668→        const ParsedData::WayGeometry* geom; // unused for sub-ways
   669→    };
   670→    // We'll adapt the BacktrackState to work with sub_ways directly
   671→    std::unordered_map<int64_t, std::vector<std::pair<size_t, bool>>> coord_adj;
   672→    for (size_t i = 0; i < sub_ways.size(); i++) {
   673→        auto& first = sub_ways[i].coords.front();
   674→        auto& last = sub_ways[i].coords.back();
   675→        coord_adj[coord_key(first.first, first.second)].push_back({i, false});
   676→        coord_adj[coord_key(last.first, last.second)].push_back({i, true});
   677→    }
   678→
   679→    std::vector<bool> used(sub_ways.size(), false);
   680→    std::vector<std::vector<std::pair<double,double>>> rings;
   681→
   682→    // Ring closure: greedy first (fast), then full backtracking retry from
   683→    // scratch if greedy left unused sub-ways. The second pass gets clean
   684→    // used flags so backtracking makes optimal branch choices.
   685→
   686→    // Backtracking with depth limit (for fallback only)
   687→    struct BacktrackState {
   688→        const std::vector<SubWay>& sways;
   689→        const std::unordered_map<int64_t, std::vector<std::pair<size_t, bool>>>& adj;
   690→        std::vector<bool>& used;
   691→        decltype(coord_key)& key_fn;
   692→        int calls;
   693→
   694→        bool try_close(int64_t first_key, int64_t last_key,
   695→                       std::vector<std::pair<size_t, bool>>& path, int depth) {
   696→            if (++calls > 100000) return false; // limit total work, not just depth
   697→            if (!path.empty() && first_key == last_key) return true;
   698→            if (depth > 200) return false;
   699→

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

    result.clear();
    for (size_t i = 0; i < pts.size(); i++) {
        if (keep[i]) result.push_back(pts[i]);
    }
    return result;
}

// --- 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) {
    // Sweep-line inspired check: sort segments by min-x, then only check
    // overlapping x-ranges. Much faster than O(n²) for large rings.
    size_t n = ring.size();
    if (n < 4) return false;

    // For small rings, brute force is fine
    if (n <= 32) {
        for (size_t i = 0; i + 1 < n; i++) {
            for (size_t j = i + 2; j + 1 < n; j++) {
                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;
    }

    // For larger rings, sort segments by min-x and prune non-overlapping
    struct Seg { double min_x, max_x; size_t idx; };
    std::vector<Seg> segs(n - 1);
    for (size_t i = 0; i + 1 < n; i++) {
        double x1 = ring[i].second, x2 = ring[i+1].second; // use lng as x
        segs[i] = {std::min(x1,x2), std::max(x1,x2), i};
    }
    std::sort(segs.begin(), segs.end(), [](const Seg& a, const Seg& b) {
        return a.min_x < b.min_x;
    });

    for (size_t a = 0; a + 1 < segs.size(); a++) {
        for (size_t b = a + 1; b < segs.size(); b++) {
            if (segs[b].min_x > segs[a].max_x) break; // no more x-overlap
            size_t i = segs[a].idx, j = segs[b].idx;
            // Skip adjacent segments
            if (j == i + 1 || i == j + 1) continue;
            if ((i == 0 && j == n - 2) || (j == 0 && i == 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;
}

// Check if a ring has duplicate coordinates (spikes/figure-8 from merged holes)
// A valid simple polygon visits each coordinate exactly once (except closing point).
static bool ring_has_duplicate_coords(const std::vector<std::pair<double,double>>& ring,
                                       decltype(std::function<int64_t(double,double)>()) coord_key_fn) {
    std::unordered_set<int64_t> seen;
    for (size_t i = 0; i + 1 < ring.size(); i++) { // skip closing point
        int64_t k = coord_key_fn(ring[i].first, ring[i].second);
        if (!seen.insert(k).second) return true; // duplicate
    }
    return false;
}

// --- 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,
    bool include_all_roles = false)
{
    // Coordinate-based ring assembly: split ways at shared internal nodes,
    // then stitch sub-ways at endpoints with backtracking.
    // This matches osmium's segment-level approach.

    // Coordinate key matching osmium's Location (int32_t nanodegrees)
    auto coord_key = [](double lat, double lng) -> int64_t {
        int32_t ilat = static_cast<int32_t>(lat * 1e7 + (lat >= 0 ? 0.5 : -0.5));
        int32_t ilng = static_cast<int32_t>(lng * 1e7 + (lng >= 0 ? 0.5 : -0.5));
        return (static_cast<int64_t>(ilat) << 32) | static_cast<uint32_t>(ilng);
    };

    // Collect coordinates from member ways.
    // First pass uses outer/empty-role ways only. If that produces 0 rings,
    // retry with ALL ways (matching osmium's check_roles=false fallback) —
    // inner ways sometimes bridge gaps between outer ways.
    std::unordered_map<int64_t, int> coord_count;
    std::vector<const std::vector<std::pair<double,double>>*> way_coords;
    for (const auto& [way_id, role] : members) {
        if (!include_all_roles && role != "outer" && !role.empty()) continue;
        auto it = way_geoms.find(way_id);
        if (it == way_geoms.end() || it->second.coords.empty()) continue;
        way_coords.push_back(&it->second.coords);
        for (const auto& [lat, lng] : it->second.coords) {
            coord_count[coord_key(lat, lng)]++;
        }
    }

    // A coordinate is a "split point" if it appears in 2+ ways' node lists.
    // Endpoints always count, internal nodes that appear in multiple ways need splitting.
    // Also, all way endpoints are natural split points.
    std::unordered_set<int64_t> split_points;
    for (auto* coords : way_coords) {
        split_points.insert(coord_key(coords->front().first, coords->front().second));
        split_points.insert(coord_key(coords->back().first, coords->back().second));
    }
    for (auto& [ck, cnt] : coord_count) {
        if (cnt > 1) split_points.insert(ck);
    }

    // Split each way at split points to create sub-ways
    struct SubWay {
        std::vector<std::pair<double,double>> coords;
    };
    std::vector<SubWay> sub_ways;
    for (auto* coords : way_coords) {
        size_t seg_start = 0;
        for (size_t i = 1; i < coords->size(); i++) {
            int64_t ck = coord_key((*coords)[i].first, (*coords)[i].second);
            if (split_points.count(ck) && i > seg_start) {
                // Create sub-way from seg_start to i (inclusive)
                SubWay sw;
                sw.coords.assign(coords->begin() + seg_start, coords->begin() + i + 1);
                if (sw.coords.size() >= 2) {
                    sub_ways.push_back(std::move(sw));
                }
                seg_start = i;
            }
        }
        // Handle remaining segment (if not already added)
        if (seg_start < coords->size() - 1) {
            SubWay sw;
            sw.coords.assign(coords->begin() + seg_start, coords->end());
            if (sw.coords.size() >= 2) {
                sub_ways.push_back(std::move(sw));
            }
        }
    }

    // Build adjacency by sub-way coordinate endpoints
    struct WayRef {
        int64_t way_id;
        const ParsedData::WayGeometry* geom; // unused for sub-ways
    };
    // We'll adapt the BacktrackState to work with sub_ways directly
    std::unordered_map<int64_t, std::vector<std::pair<size_t, bool>>> coord_adj;
    for (size_t i = 0; i < sub_ways.size(); i++) {
        auto& first = sub_ways[i].coords.front();
        auto& last = sub_ways[i].coords.back();
        coord_adj[coord_key(first.first, first.second)].push_back({i, false});
        coord_adj[coord_key(last.first, last.second)].push_back({i, true});
    }

    std::vector<bool> used(sub_ways.size(), false);
    std::vector<std::vector<std::pair<double,double>>> rings;

    // Ring closure: greedy first (fast), then full backtracking retry from
    // scratch if greedy left unused sub-ways. The second pass gets clean
    // used flags so backtracking makes optimal branch choices.

    // Backtracking with depth limit (for fallback only)
    struct BacktrackState {
        const std::vector<SubWay>& sways;
        const std::unordered_map<int64_t, std::vector<std::pair<size_t, bool>>>& adj;
        std::vector<bool>& used;
        decltype(coord_key)& key_fn;
        int calls;

        bool try_close(int64_t first_key, int64_t last_key,
                       std::vector<std::pair<size_t, bool>>& path, int depth) {
            if (++calls > 100000) return false; // limit total work, not just depth
            if (!path.empty() && first_key == last_key) return true;
            if (depth > 200) return false;