Now let me also look at the S2 sort+group phase — can we use parallel sort?


2942:                std::sort(all.begin(), all.end(), [](const CellItem& a, const CellItem& b) {


  2930→            auto build_cell_map = [](std::vector<std::vector<CellItem>>& thread_pairs,
  2931→                                      std::unordered_map<uint64_t, std::vector<uint32_t>>& out) {
  2932→                // Concatenate all thread-local vectors
  2933→                size_t total = 0;
  2934→                for (auto& v : thread_pairs) total += v.size();
  2935→                std::vector<CellItem> all;
  2936→                all.reserve(total);
  2937→                for (auto& v : thread_pairs) {
  2938→                    all.insert(all.end(), v.begin(), v.end());
  2939→                    v.clear(); v.shrink_to_fit();
  2940→                }
  2941→                // Sort by cell_id
  2942→                std::sort(all.begin(), all.end(), [](const CellItem& a, const CellItem& b) {
  2943→                    return a.cell_id < b.cell_id;
  2944→                });
  2945→                // Group into cell map
  2946→                out.reserve(total / 2); // rough estimate of unique cells
  2947→                for (size_t i = 0; i < all.size(); ) {
  2948→                    size_t j = i;
  2949→                    while (j < all.size() && all[j].cell_id == all[i].cell_id) j++;
  2950→                    auto& vec = out[all[i].cell_id];
  2951→                    vec.reserve(j - i);
  2952→                    for (size_t k = i; k < j; k++) vec.push_back(all[k].item_id);
  2953→                    i = j;
  2954→                }

            auto build_cell_map = [](std::vector<std::vector<CellItem>>& thread_pairs,
                                      std::unordered_map<uint64_t, std::vector<uint32_t>>& out) {
                // Concatenate all thread-local vectors
                size_t total = 0;
                for (auto& v : thread_pairs) total += v.size();
                std::vector<CellItem> all;
                all.reserve(total);
                for (auto& v : thread_pairs) {
                    all.insert(all.end(), v.begin(), v.end());
                    v.clear(); v.shrink_to_fit();
                }
                // Sort by cell_id
                std::sort(all.begin(), all.end(), [](const CellItem& a, const CellItem& b) {
                    return a.cell_id < b.cell_id;
                });
                // Group into cell map
                out.reserve(total / 2); // rough estimate of unique cells
                for (size_t i = 0; i < all.size(); ) {
                    size_t j = i;
                    while (j < all.size() && all[j].cell_id == all[i].cell_id) j++;
                    auto& vec = out[all[i].cell_id];
                    vec.reserve(j - i);
                    for (size_t k = i; k < j; k++) vec.push_back(all[k].item_id);
                    i = j;
                }


The sort of the `all` vector (86M entries for Europe, ~332M for planet) is the bottleneck. Each thread's pairs are already sorted by cell_id (since ways are processed in PBF order which is roughly geographic). We can use a **merge sort** of pre-sorted runs instead of sorting the entire array.

Also, grouping into `unordered_map` at line 2946 is still expensive — we allocate millions of small vectors. Since the data is already sorted by cell_id, we could skip the hash map entirely and write the entries directly from the sorted array. But that requires restructuring the write path.

Let me do the parallel merge sort first:


Edit

            auto build_cell_map = [](std::vector<std::vector<CellItem>>& thread_pairs,
                                      std::unordered_map<uint64_t, std::vector<uint32_t>>& out) {
                // Concatenate all thread-local vectors
                size_t total = 0;
                for (auto& v : thread_pairs) total += v.size();
                std::vector<CellItem> all;
                all.reserve(total);
                for (auto& v : thread_pairs) {
                    all.insert(all.end(), v.begin(), v.end());
                    v.clear(); v.shrink_to_fit();
                }
                // Sort by cell_id
                std::sort(all.begin(), all.end(), [](const CellItem& a, const CellItem& b) {
                    return a.cell_id < b.cell_id;
                });
                // Group into cell map
                out.reserve(total / 2); // rough estimate of unique cells
                for (size_t i = 0; i < all.size(); ) {
                    size_t j = i;
                    while (j < all.size() && all[j].cell_id == all[i].cell_id) j++;
                    auto& vec = out[all[i].cell_id];
                    vec.reserve(j - i);
                    for (size_t k = i; k < j; k++) vec.push_back(all[k].item_id);
                    i = j;
                }
            };

            auto build_cell_map = [](std::vector<std::vector<CellItem>>& thread_pairs,
                                      std::unordered_map<uint64_t, std::vector<uint32_t>>& out) {
                // Sort each thread-local vector (they're mostly sorted already
                // since ways are processed in PBF geographic order)
                for (auto& v : thread_pairs) {
                    std::sort(v.begin(), v.end(), [](const CellItem& a, const CellItem& b) {
                        return a.cell_id < b.cell_id;
                    });
                }

                // K-way merge of pre-sorted runs (much faster than sorting entire array)
                size_t total = 0;
                for (auto& v : thread_pairs) total += v.size();

                // Use iterators for each thread's sorted data
                struct RunState { size_t idx; size_t end; size_t run; };
                std::vector<RunState> runs;
                std::vector<CellItem*> bases;
                for (size_t r = 0; r < thread_pairs.size(); r++) {
                    if (!thread_pairs[r].empty()) {
                        runs.push_back({0, thread_pairs[r].size(), r});
                        bases.push_back(thread_pairs[r].data());
                    }
                }

                // Simple sequential merge into cell map (k-way merge is I/O bound anyway)
                out.reserve(total / 2);
                while (!runs.empty()) {
                    // Find minimum across all run heads
                    size_t min_r = 0;
                    uint64_t min_key = bases[runs[0].run][runs[0].idx].cell_id;
                    for (size_t r = 1; r < runs.size(); r++) {
                        uint64_t k = bases[runs[r].run][runs[r].idx].cell_id;
                        if (k < min_key) { min_key = k; min_r = r; }
                    }

                    // Collect all items with this cell_id from all runs
                    auto& vec = out[min_key];
                    for (size_t r = 0; r < runs.size(); r++) {
                        auto* base = bases[runs[r].run];
                        while (runs[r].idx < runs[r].end && base[runs[r].idx].cell_id == min_key) {
                            vec.push_back(base[runs[r].idx].item_id);
                            runs[r].idx++;
                        }
                    }

                    // Remove exhausted runs
                    runs.erase(std::remove_if(runs.begin(), runs.end(),
                        [](const RunState& r) { return r.idx >= r.end; }), runs.end());
                }

                // Free thread-local data
                for (auto& v : thread_pairs) { v.clear(); v.shrink_to_fit(); }
            };