grep -n "way_pairs\|addr_pairs\|CellItem\|struct.*CellItem" /root/traccar-geocoder/builder/src/build_index.cpp | head -50


982:            struct CellItem { uint64_t cell_id; uint32_t item_id; };
989:            std::vector<std::vector<CellItem>> way_pairs(num_threads);
995:                        auto& local = way_pairs[t];
1024:            std::vector<std::vector<CellItem>> interp_pairs(num_threads);
1058:            // Concatenate + sort helper for flat CellItem pairs
1062:                std::vector<std::vector<CellItem>>& thread_pairs,
1064:                std::vector<CellItemPair>& sorted_out
1070:                std::vector<std::vector<CellItemPair>> chunks(thread_pairs.size());
1080:                            std::sort(dst.begin(), dst.end(), [](const CellItemPair& a, const CellItemPair& b) {
1091:                auto cmp = [](const CellItemPair& a, const CellItemPair& b) {
1139:                parallel_sort_and_build(way_pairs, data.cell_to_ways, data.sorted_way_cells);
1210:                std::vector<CellItemPair> pairs;
1235:        auto rebuild_map_parallel = [](const std::vector<CellItemPair>& sorted,
1366:        auto precompute_masks = [&continent_polys](const std::vector<CellItemPair>& sorted) -> std::vector<uint8_t> {


Let me read the section where way_pairs are built:


   975→            log_phase("Admin assembly", _pt, _cpu);
   976→            std::cerr << "Computing S2 cells for ways with " << num_threads << " threads..." << std::endl;
   977→
   978→            // Flat-array approach: threads emit (cell_id, item_id) pairs into
   979→            // thread-local vectors, then we sort globally and group by cell_id.
   980→            // This avoids hash maps entirely — sort is cache-friendly O(n log n).
   981→
   982→            struct CellItem { uint64_t cell_id; uint32_t item_id; };
   983→
   984→            auto _s2t = std::chrono::steady_clock::now();
   985→            auto _s2cpu = CpuTicks::now();
   986→
   987→            // Process streets: emit (cell_id, way_id) pairs
   988→            std::cerr << "  Processing " << data.deferred_ways.size() << " street ways..." << std::endl;
   989→            std::vector<std::vector<CellItem>> way_pairs(num_threads);
   990→            {
   991→                std::atomic<size_t> way_idx{0};
   992→                std::vector<std::thread> threads;
   993→                for (unsigned int t = 0; t < num_threads; t++) {
   994→                    threads.emplace_back([&, t]() {
   995→                        auto& local = way_pairs[t];
   996→                        local.reserve(data.deferred_ways.size() / num_threads * 3);
   997→                        std::vector<S2CellId> edge_cells;
   998→                        std::vector<uint64_t> way_cells;
   999→                        while (true) {
  1000→                            size_t i = way_idx.fetch_add(1);
  1001→                            if (i >= data.deferred_ways.size()) break;
  1002→                            const auto& dw = data.deferred_ways[i];
  1003→                            way_cells.clear();
  1004→                            for (uint8_t j = 0; j + 1 < dw.node_count; j++) {
  1005→                                const auto& n1 = data.street_nodes[dw.node_offset + j];
  1006→                                const auto& n2 = data.street_nodes[dw.node_offset + j + 1];
  1007→                                cover_edge(n1.lat, n1.lng, n2.lat, n2.lng, edge_cells);
  1008→                                for (const auto& c : edge_cells) way_cells.push_back(c.id());
  1009→                            }
  1010→                            std::sort(way_cells.begin(), way_cells.end());
  1011→                            way_cells.erase(std::unique(way_cells.begin(), way_cells.end()), way_cells.end());
  1012→                            for (uint64_t cell_id : way_cells) {
  1013→                                local.push_back({cell_id, dw.way_id});
  1014→                            }
  1015→                        }
  1016→                    });
  1017→                }
  1018→                for (auto& t : threads) t.join();
  1019→            }
  1020→            log_phase("  S2: street ways (parallel)", _s2t, _s2cpu);
  1021→
  1022→            // Process interpolations: emit (cell_id, interp_id) pairs
  1023→            std::cerr << "  Processing " << data.deferred_interps.size() << " interpolation ways..." << std::endl;
  1024→            std::vector<std::vector<CellItem>> interp_pairs(num_threads);
  1025→            {
  1026→                std::atomic<size_t> interp_idx{0};
  1027→                std::vector<std::thread> threads;
  1028→                for (unsigned int t = 0; t < num_threads; t++) {
  1029→                    threads.emplace_back([&, t]() {
  1030→                        auto& local = interp_pairs[t];
  1031→                        std::vector<S2CellId> edge_cells;
  1032→                        std::vector<uint64_t> way_cells;
  1033→                        while (true) {
  1034→                            size_t i = interp_idx.fetch_add(1);
  1035→                            if (i >= data.deferred_interps.size()) break;
  1036→                            const auto& di = data.deferred_interps[i];
  1037→                            way_cells.clear();
  1038→                            for (uint8_t j = 0; j + 1 < di.node_count; j++) {
  1039→                                const auto& n1 = data.interp_nodes[di.node_offset + j];
  1040→                                const auto& n2 = data.interp_nodes[di.node_offset + j + 1];
  1041→                                cover_edge(n1.lat, n1.lng, n2.lat, n2.lng, edge_cells);
  1042→                                for (const auto& c : edge_cells) way_cells.push_back(c.id());
  1043→                            }
  1044→                            std::sort(way_cells.begin(), way_cells.end());
  1045→                            way_cells.erase(std::unique(way_cells.begin(), way_cells.end()), way_cells.end());
  1046→                            for (uint64_t cell_id : way_cells) {
  1047→                                local.push_back({cell_id, di.interp_id});
  1048→                            }
  1049→                        }
  1050→                    });
  1051→                }
  1052→                for (auto& t : threads) t.join();
  1053→            }
  1054→            log_phase("  S2: interp ways (parallel)", _s2t, _s2cpu);
  1055→
  1056→            // Merge thread-local pairs into single vectors, sort, build cell maps
  1057→            std::cerr << "  Sorting and grouping cell pairs..." << std::endl;
  1058→            // Concatenate + sort helper for flat CellItem pairs
  1059→            // Parallel sort each thread's pairs, then k-way merge directly into
  1060→            // hash map + sorted output. No intermediate merged vector needed.
  1061→            auto parallel_sort_and_build = [](
  1062→                std::vector<std::vector<CellItem>>& thread_pairs,
  1063→                std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map,
  1064→                std::vector<CellItemPair>& sorted_out
  1065→            ) {
  1066→                // Step 1: Convert + sort each thread's data in parallel
  1067→                size_t total = 0;
  1068→                for (auto& v : thread_pairs) total += v.size();
  1069→
  1070→                std::vector<std::vector<CellItemPair>> chunks(thread_pairs.size());
  1071→                {
  1072→                    std::vector<std::thread> sort_threads;
  1073→                    for (size_t t = 0; t < thread_pairs.size(); t++) {
  1074→                        sort_threads.emplace_back([&, t]() {


Excellent! Now let me look at the address points S2 processing. Let me search for where addr_points cells are determined:


Bash

grep -n "cell_to_addrs\|address.*cell\|addr.*S2" /root/traccar-geocoder/builder/src/build_index.cpp | head -40


418:                    std::vector<std::pair<double, double>> building_addr_coords; // lat,lng for S2 cell
1174:                sorted_cells.reserve(data.cell_to_addrs.size());
1175:                for (auto& [cell_id, ids] : data.cell_to_addrs)
1194:                                auto& ids = data.cell_to_addrs[sorted_cells[i]];
1206:                for (auto& [_, ids] : data.cell_to_addrs) total_pairs += ids.size();
1213:                    auto& ids = data.cell_to_addrs[cell_id];


Let me read the address point deduplication section:


  1165→            // Convert addr hash map to sorted pairs.
  1166→            // Sort cell IDs only (30M unique), then build pairs in sorted order.
  1167→            // Much faster than sorting all 160M pairs.
  1168→            auto f2 = std::async(std::launch::async, [&] {
  1169→                auto _dt = std::chrono::steady_clock::now();
  1170→                auto _dc = CpuTicks::now();
  1171→
  1172→                // Step 1: Extract cell IDs from hash map
  1173→                std::vector<uint64_t> sorted_cells;
  1174→                sorted_cells.reserve(data.cell_to_addrs.size());
  1175→                for (auto& [cell_id, ids] : data.cell_to_addrs)
  1176→                    sorted_cells.push_back(cell_id);
  1177→                log_phase("    Dedup: extract cell IDs", _dt, _dc);
  1178→
  1179→                // Step 2: Sort cell IDs
  1180→                std::sort(sorted_cells.begin(), sorted_cells.end());
  1181→                log_phase("    Dedup: sort cell IDs", _dt, _dc);
  1182→
  1183→                // Step 3: Parallel dedup within each cell
  1184→                {
  1185→                    unsigned nthreads = std::thread::hardware_concurrency();
  1186→                    size_t chunk = (sorted_cells.size() + nthreads - 1) / nthreads;
  1187→                    std::vector<std::thread> threads;
  1188→                    for (unsigned t = 0; t < nthreads; t++) {
  1189→                        size_t start = t * chunk;
  1190→                        size_t end = std::min(start + chunk, sorted_cells.size());
  1191→                        if (start >= sorted_cells.size()) break;
  1192→                        threads.emplace_back([&, start, end]() {
  1193→                            for (size_t i = start; i < end; i++) {
  1194→                                auto& ids = data.cell_to_addrs[sorted_cells[i]];
  1195→                                std::sort(ids.begin(), ids.end());
  1196→                                ids.erase(std::unique(ids.begin(), ids.end()), ids.end());
  1197→                            }
  1198→                        });
  1199→                    }
  1200→                    for (auto& t : threads) t.join();
  1201→                }
  1202→                log_phase("    Dedup: per-cell sort+dedup", _dt, _dc);
  1203→
  1204→                // Step 4: Count total pairs
  1205→                size_t total_pairs = 0;
  1206→                for (auto& [_, ids] : data.cell_to_addrs) total_pairs += ids.size();
  1207→                log_phase("    Dedup: count pairs", _dt, _dc);
  1208→
  1209→                // Step 5: Build sorted pairs
  1210→                std::vector<CellItemPair> pairs;
  1211→                pairs.reserve(total_pairs);
  1212→                for (uint64_t cell_id : sorted_cells) {
  1213→                    auto& ids = data.cell_to_addrs[cell_id];
  1214→                    for (auto id : ids) pairs.push_back({cell_id, id});
  1215→                }
  1216→                data.sorted_addr_cells = std::move(pairs);
  1217→                log_phase("    Dedup: build sorted pairs", _dt, _dc);
  1218→            });
  1219→            auto f4 = std::async(std::launch::async, [&]{ deduplicate(data.cell_to_admin); });
  1220→            f2.get(); f4.get();
  1221→        }
  1222→
  1223→        // Save cache if requested
  1224→        if (!save_cache_path.empty()) {
  1225→            serialize_cache(data, save_cache_path);
  1226→        }
  1227→    }
  1228→
  1229→    // --- Rebuild hash maps from sorted pairs if needed for cache saving ---
  1230→    // (Continent filtering now uses sorted pairs directly, so no rebuild needed for that.)
  1231→    if (!save_cache_path.empty()) {
  1232→        // Parallel rebuild: split sorted pairs into chunks, each thread builds
  1233→        // a partial map, then merge. Sorted pairs are grouped by cell_id so we
  1234→        // can split at cell boundaries for zero-conflict parallel insertion.


Perfect! Now let me create a summary document with my findings. Let me also check how the addr_points are written to the binary file to confirm the ordering:


   302→        if (write_addresses) {
   303→            write_futures.push_back(std::async(std::launch::async, [&] {
   304→                std::ofstream f(output_dir + "/addr_points.bin", std::ios::binary);
   305→                f.write(reinterpret_cast<const char*>(data.addr_points.data()), data.addr_points.size() * sizeof(AddrPoint));
   306→            }));
   307→            write_futures.push_back(std::async(std::launch::async, [&] {
   308→                std::ofstream f(output_dir + "/interp_ways.bin", std::ios::binary);
   309→                f.write(reinterpret_cast<const char*>(data.interp_ways.data()), data.interp_ways.size() * sizeof(InterpWay));
   310→            }));
   311→            write_futures.push_back(std::async(std::launch::async, [&] {


Perfect! Now let me check how street_ways are written:


   293→    if (write_streets) {
   294→        write_futures.push_back(std::async(std::launch::async, [&] {
   295→            std::ofstream f(output_dir + "/street_ways.bin", std::ios::binary);
   296→            f.write(reinterpret_cast<const char*>(data.ways.data()), data.ways.size() * sizeof(WayHeader));
   297→        }));
   298→        write_futures.push_back(std::async(std::launch::async, [&] {
   299→            std::ofstream f(output_dir + "/street_nodes.bin", std::ios::binary);
   300→            f.write(reinterpret_cast<const char*>(data.street_nodes.data()), data.street_nodes.size() * sizeof(NodeCoord));
   301→        }));
   302→        if (write_addresses) {


Perfect! Now I have all the information I need. Let me compile my findings into a comprehensive report.

## Summary: ID Assignment and Ordering in build_index.cpp

Based on my analysis of the code, here's how address points and street ways get their IDs and ordering:

### 1. **Address Points Collection (Pass 2 - Node Scanning)**

**Location:** `/root/traccar-geocoder/builder/src/build_index.cpp`, lines 339-401

During Pass 2, nodes are scanned in a streaming fashion with thread-local data collection:

- **Thread-local collection:** Each thread maintains a local vector of `addr_coords` and `addr_strings` (lines 342-346)
- **Streaming callback:** For each node with `addr:housenumber` and `addr:street` tags, coordinates and strings are appended to the thread-local vectors (lines 379-381)
- **Sequential merge:** After all PBF blocks are processed, the thread-local data from all threads is merged sequentially into `data.addr_points` (lines 389-399)

```cpp
// From lines 392-396:
for (size_t j = 0; j < local.addr_coords.size(); j++) {
    uint64_t dummy = 0;
    add_addr_point(data, local.addr_coords[j].first, local.addr_coords[j].second,
                   local.addr_strings[j].first.c_str(),
                   local.addr_strings[j].second.c_str(), dummy);
}
```

### 2. **Address Point ID Assignment**

**Location:** `/root/traccar-geocoder/builder/src/s2_helpers.cpp`, lines 229-247

Each address point gets its ID based on its position in `data.addr_points`:

```cpp
uint32_t addr_id = static_cast<uint32_t>(data.addr_points.size());
data.addr_points.push_back({
    static_cast<float>(lat),
    static_cast<float>(lng),
    data.string_pool.intern(housenumber),
    data.string_pool.intern(street)
});
```

The addr_id is simply the vector index at the time of insertion. Simultaneously, the address point is also added to the S2 cell index:
```cpp
S2CellId cell = point_to_cell(lat, lng);
data.cell_to_addrs[cell.id()].push_back(addr_id);
```

### 3. **Storage and Sorting Before Writing to addr_points.bin**

**Key finding:** Address points are **NOT sorted** before writing to the binary file.

- **No explicit sorting:** There is no sort operation on `data.addr_points` before writing
- **Direct write:** addr_points.bin is written directly from the vector as-is (cell_index.cpp, lines 304-305):
  ```cpp
  std::ofstream f(output_dir + "/addr_points.bin", std::ios::binary);
  f.write(reinterpret_cast<const char*>(data.addr_points.data()),
          data.addr_points.size() * sizeof(AddrPoint));
  ```

- **Separate index:** The S2 cell mapping (`cell_to_addrs`) is converted to sorted pairs (`sorted_addr_cells`) for indexing purposes only, not for reordering addr_points.bin

### 4. **Ordering in addr_points.bin**

**Deterministic based on:**
- **PBF file processing order:** Nodes are processed sequentially through PBF blocks as they appear in the file
- **Thread assignment:** Thread-local buffers collect nodes in parallel, but the merge phase (lines 391-399) processes thread-local data in thread index order (for loop over `ntld`)
- **Thread pool iteration:** Within each thread's data, addresses appear in the order they were encountered during streaming

**NOT based on:**
- Hash map iteration (not used for ordering)
- S2 cell order (S2 cells are computed but used only for indexing, not for reordering records)

**Result:** The ordering is **deterministic** and depends on:
1. The order of nodes in the PBF input file
2. The order threads are assigned during multi-threaded processing
3. The order of thread data merging (thread-0 data, then thread-1, etc.)

### 5. **Street Ways Ordering**

**Location:** `/root/traccar-geocoder/builder/src/build_index.cpp`, lines 607-619

Street ways follow a similar pattern:

- **Collection:** Ways are collected in thread-local data structures during parallel way processing (Pass 2b)
- **Sequential merge:** Thread-local ways are merged sequentially:
  ```cpp
  for (auto& local : tld) {
      uint32_t way_base = static_cast<uint32_t>(data.ways.size());
      // ...
      for (size_t i = 0; i < local.ways.size(); i++) {
          auto h = local.ways[i];
          h.node_offset += node_base;
          h.name_id = data.string_pool.intern(local.way_strings[i]);
          data.ways.push_back(h);  // <-- way_id = data.ways.size() at insertion
      }
  }
  ```

- **Writing:** street_ways.bin is written directly without sorting (cell_index.cpp, lines 295-296):
  ```cpp
  std::ofstream f(output_dir + "/street_ways.bin", std::ios::binary);
  f.write(reinterpret_cast<const char*>(data.ways.data()),
          data.ways.size() * sizeof(WayHeader));
  ```

**Ordering determinism:** Same as addr_points - **deterministic** based on PBF file order and thread merging order.

### 6. **S2 Cell Index Ordering**

The sorted pairs (`sorted_addr_cells`, `sorted_way_cells`) used for addr_entries.bin and street_entries.bin ARE sorted, but with a specific approach:

**Location:** `/root/traccar-geocoder/builder/src/build_index.cpp`, lines 1061-1133

- **Parallel sort:** Each thread sorts its (cell_id, item_id) pairs independently
- **Parallel tree merge:** Adjacent sorted runs are merged pairwise in parallel using `std::inplace_merge`
- **Sorting order:**
  ```cpp
  std::sort(dst.begin(), dst.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);
  });
  ```

This means the *index* files (addr_entries.bin and street_entries.bin) ARE sorted by S2 cell ID, but the *data* files (addr_points.bin and street_ways.bin) maintain their original order.

### Summary Table

| Aspect | addr_points.bin | street_ways.bin |
|--------|-----------------|-----------------|
| **ID assignment** | Vector index at push_back | Vector index at push_back |
| **Sorting before write** | NO | NO |
| **Deterministic** | YES | YES |
| **Depends on** | PBF order + thread merge order | PBF order + thread merge order |
| **S2 cell used for** | Indexing only (separate index files) | Indexing only (separate index files) |
| **Hash map iteration** | NOT used for ordering | NOT used for ordering |