    33→ParsedData filter_by_bbox(const ParsedData& full, const ContinentBBox& bbox) {
    34→    ParsedData out;
    35→
    36→    std::cerr << "  DEBUG filter_by_bbox: ways_sorted=" << full.sorted_way_cells.size()
    37→              << " addrs_sorted=" << full.sorted_addr_cells.size()
    38→              << " interps_sorted=" << full.sorted_interp_cells.size()
    39→              << " ways_map=" << full.cell_to_ways.size()
    40→              << " addrs_map=" << full.cell_to_addrs.size()
    41→              << " interps_map=" << full.cell_to_interps.size()
    42→              << " admin_map=" << full.cell_to_admin.size()
    43→              << std::endl;
    44→
    45→    // Filter from hash map
    46→    auto filter_cells_map = [&](const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map,
    47→                                bool mask_interior = false) {
    48→        std::cerr << "    DEBUG filter_cells_map: size=" << cell_map.size() << std::endl;
    49→        std::unordered_set<uint32_t> ids;
    50→        for (const auto& [cell_id, cell_ids] : cell_map) {
    51→            if (cell_in_bbox(cell_id, bbox)) {
    52→                for (uint32_t id : cell_ids)
    53→                    ids.insert(mask_interior ? (id & ID_MASK) : id);
    54→            }
    55→        }
    56→        return ids;
    57→    };
    58→
    59→    // Filter from sorted pairs — parallel chunked scan.
    60→    // Split the sorted array into N chunks at cell boundaries, each thread
    61→    // builds its own ID set, then merge.
    62→    auto filter_cells_sorted = [&](const std::vector<CellItemPair>& sorted,
    63→                                   bool mask_interior = false) {
    64→        if (sorted.empty()) return std::unordered_set<uint32_t>{};
    65→
    66→        unsigned nthreads = std::max(1u, std::thread::hardware_concurrency() / 4);
    67→        size_t chunk = (sorted.size() + nthreads - 1) / nthreads;
    68→        std::cerr << "    DEBUG filter_cells_sorted: size=" << sorted.size()
    69→                  << " nthreads=" << nthreads << " chunk=" << chunk << std::endl;
    70→
    71→        // Find chunk boundaries at cell_id transitions
    72→        std::vector<size_t> bounds = {0};
    73→        for (unsigned t = 1; t < nthreads; t++) {
    74→            size_t target = t * chunk;
    75→            if (target >= sorted.size()) break;
    76→            while (target < sorted.size() && sorted[target].cell_id == sorted[target-1].cell_id)
    77→                target++;
    78→            if (target < sorted.size()) bounds.push_back(target);
    79→        }
    80→        bounds.push_back(sorted.size());
    81→
    82→        std::vector<std::unordered_set<uint32_t>> thread_ids(bounds.size() - 1);
    83→        std::vector<std::thread> threads;
    84→        for (size_t t = 0; t + 1 < bounds.size(); t++) {
    85→            threads.emplace_back([&, t]() {
    86→                auto& local = thread_ids[t];
    87→                for (size_t i = bounds[t]; i < bounds[t+1]; ) {
    88→                    uint64_t cell_id = sorted[i].cell_id;
    89→                    bool match = cell_in_bbox(cell_id, bbox);
    90→                    while (i < bounds[t+1] && sorted[i].cell_id == cell_id) {
    91→                        if (match) {
    92→                            uint32_t id = sorted[i].item_id;
    93→                            local.insert(mask_interior ? (id & ID_MASK) : id);
    94→                        }
    95→                        i++;
    96→                    }
    97→                }
    98→            });
    99→        }
   100→        std::cerr << "    DEBUG: spawned " << threads.size() << " threads, "
   101→                  << bounds.size() - 1 << " chunks" << std::endl;
   102→        for (auto& t : threads) t.join();
   103→
   104→        // Merge thread-local sets
   105→        size_t largest = 0;
   106→        for (size_t t = 0; t < thread_ids.size(); t++)
   107→            if (thread_ids[t].size() > thread_ids[largest].size()) largest = t;
   108→        auto& merged = thread_ids[largest];
   109→        for (size_t t = 0; t < thread_ids.size(); t++) {
   110→            if (t == largest) continue;
   111→            for (auto id : thread_ids[t]) merged.insert(id);
   112→        }
   113→        return std::move(merged);
   114→    };
   115→
   116→    // Use sorted pairs where available (faster linear scan), fall back to hash maps
   117→    auto f_ways = std::async(std::launch::async, [&]{
   118→        return !full.sorted_way_cells.empty()
   119→            ? filter_cells_sorted(full.sorted_way_cells)
   120→            : filter_cells_map(full.cell_to_ways);
   121→    });
   122→    auto f_addrs = std::async(std::launch::async, [&]{
   123→        return !full.sorted_addr_cells.empty()
   124→            ? filter_cells_sorted(full.sorted_addr_cells)
   125→            : filter_cells_map(full.cell_to_addrs);
   126→    });
   127→    auto f_interps = std::async(std::launch::async, [&]{
   128→        return !full.sorted_interp_cells.empty()
   129→            ? filter_cells_sorted(full.sorted_interp_cells)
   130→            : filter_cells_map(full.cell_to_interps);
   131→    });
   132→    auto f_admin = std::async(std::launch::async, [&]{
   133→        return filter_cells_map(full.cell_to_admin, true);
   134→    });
   135→
   136→    auto used_way_ids = f_ways.get();
   137→    auto used_addr_ids = f_addrs.get();
   138→    auto used_interp_ids = f_interps.get();
   139→    auto used_admin_ids = f_admin.get();
   140→
   141→    // Remap all 4 data types in parallel — each builds its own vectors and remap table
   142→    std::unordered_map<uint32_t, uint32_t> way_remap, addr_remap, interp_remap, admin_remap;
   143→
   144→    // Ways
   145→    auto f_remap_ways = std::async(std::launch::async, [&]() {
   146→        std::vector<uint32_t> sorted_ids(used_way_ids.begin(), used_way_ids.end());
   147→        std::sort(sorted_ids.begin(), sorted_ids.end());
   148→        std::vector<WayHeader> ways;
   149→        std::vector<NodeCoord> nodes;
   150→        ways.reserve(sorted_ids.size());
   151→        nodes.reserve(sorted_ids.size() * 5);
   152→        std::unordered_map<uint32_t, uint32_t> remap;
   153→        remap.reserve(sorted_ids.size());
   154→        for (uint32_t old_id : sorted_ids) {
   155→            remap[old_id] = static_cast<uint32_t>(ways.size());
   156→            const auto& w = full.ways[old_id];
   157→            WayHeader nw = w;
   158→            nw.node_offset = static_cast<uint32_t>(nodes.size());
   159→            ways.push_back(nw);
   160→            for (uint8_t n = 0; n < w.node_count; n++)
   161→                nodes.push_back(full.street_nodes[w.node_offset + n]);
   162→        }
   163→        return std::make_tuple(std::move(remap), std::move(ways), std::move(nodes));
   164→    });
   165→
   166→    // Addrs
   167→    auto f_remap_addrs = std::async(std::launch::async, [&]() {
   168→        std::vector<uint32_t> sorted_ids(used_addr_ids.begin(), used_addr_ids.end());
   169→        std::sort(sorted_ids.begin(), sorted_ids.end());
   170→        std::vector<AddrPoint> addrs;
   171→        addrs.reserve(sorted_ids.size());
   172→        std::unordered_map<uint32_t, uint32_t> remap;
   173→        remap.reserve(sorted_ids.size());
   174→        for (uint32_t old_id : sorted_ids) {
   175→            remap[old_id] = static_cast<uint32_t>(addrs.size());
   176→            addrs.push_back(full.addr_points[old_id]);
   177→        }
   178→        return std::make_tuple(std::move(remap), std::move(addrs));
   179→    });
   180→
   181→    // Interps
   182→    auto f_remap_interps = std::async(std::launch::async, [&]() {
   183→        std::vector<uint32_t> sorted_ids(used_interp_ids.begin(), used_interp_ids.end());
   184→        std::sort(sorted_ids.begin(), sorted_ids.end());
   185→        std::vector<InterpWay> iways;
   186→        std::vector<NodeCoord> inodes;
   187→        iways.reserve(sorted_ids.size());
   188→        std::unordered_map<uint32_t, uint32_t> remap;
   189→        remap.reserve(sorted_ids.size());
   190→        for (uint32_t old_id : sorted_ids) {
   191→            remap[old_id] = static_cast<uint32_t>(iways.size());
   192→            const auto& iw = full.interp_ways[old_id];
   193→            InterpWay niw = iw;
   194→            niw.node_offset = static_cast<uint32_t>(inodes.size());
   195→            iways.push_back(niw);
   196→            for (uint8_t n = 0; n < iw.node_count; n++)
   197→                inodes.push_back(full.interp_nodes[iw.node_offset + n]);
   198→        }
   199→        return std::make_tuple(std::move(remap), std::move(iways), std::move(inodes));
   200→    });
   201→
   202→    // Admins
   203→    auto f_remap_admins = std::async(std::launch::async, [&]() {
   204→        std::vector<uint32_t> sorted_ids(used_admin_ids.begin(), used_admin_ids.end());
   205→        std::sort(sorted_ids.begin(), sorted_ids.end());
   206→        std::vector<AdminPolygon> polys;
   207→        std::vector<NodeCoord> verts;
   208→        polys.reserve(sorted_ids.size());
   209→        std::unordered_map<uint32_t, uint32_t> remap;
   210→        remap.reserve(sorted_ids.size());
   211→        for (uint32_t old_id : sorted_ids) {
   212→            remap[old_id] = static_cast<uint32_t>(polys.size());
   213→            const auto& ap = full.admin_polygons[old_id];
   214→            AdminPolygon nap = ap;
   215→            nap.vertex_offset = static_cast<uint32_t>(verts.size());
   216→            polys.push_back(nap);
   217→            for (uint32_t v = 0; v < ap.vertex_count; v++)
   218→                verts.push_back(full.admin_vertices[ap.vertex_offset + v]);
   219→        }
   220→        return std::make_tuple(std::move(remap), std::move(polys), std::move(verts));
   221→    });
   222→
   223→    // Collect results
   224→    {
   225→        auto [wr, ways, nodes] = f_remap_ways.get();
   226→        way_remap = std::move(wr);
   227→        out.ways = std::move(ways);
   228→        out.street_nodes = std::move(nodes);
   229→    }
   230→    {
   231→        auto [ar, addrs] = f_remap_addrs.get();
   232→        addr_remap = std::move(ar);
   233→        out.addr_points = std::move(addrs);
   234→    }
   235→    {
   236→        auto [ir, iways, inodes] = f_remap_interps.get();
   237→        interp_remap = std::move(ir);
   238→        out.interp_ways = std::move(iways);
   239→        out.interp_nodes = std::move(inodes);
   240→    }
   241→    {
   242→        auto [ar, polys, verts] = f_remap_admins.get();
   243→        admin_remap = std::move(ar);
   244→        out.admin_polygons = std::move(polys);
   245→        out.admin_vertices = std::move(verts);
   246→    }
   247→
   248→    // Remap cell maps in parallel (each builds an independent output map)
   249→    auto remap_cells = [&](const std::unordered_map<uint64_t, std::vector<uint32_t>>& src,
   250→                           const std::unordered_map<uint32_t, uint32_t>& remap,
   251→                           std::unordered_map<uint64_t, std::vector<uint32_t>>& dst,
   252→                           bool handle_flags = false) {
   253→        for (const auto& [cell_id, ids] : src) {
   254→            if (!cell_in_bbox(cell_id, bbox)) continue;
   255→            std::vector<uint32_t> new_ids;
   256→            for (uint32_t id : ids) {
   257→                uint32_t raw_id = handle_flags ? (id & ID_MASK) : id;
   258→                uint32_t flags = handle_flags ? (id & INTERIOR_FLAG) : 0;
   259→                auto it = remap.find(raw_id);
   260→                if (it != remap.end()) new_ids.push_back(it->second | flags);
   261→            }
   262→            if (!new_ids.empty()) dst[cell_id] = std::move(new_ids);
   263→        }
   264→    };
   265→
   266→    {
   267→        auto f1 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_ways, way_remap, out.cell_to_ways); });
   268→        auto f2 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_addrs, addr_remap, out.cell_to_addrs); });
   269→        auto f3 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_interps, interp_remap, out.cell_to_interps); });
   270→        auto f4 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_admin, admin_remap, out.cell_to_admin, true); });
   271→        f1.get(); f2.get(); f3.get(); f4.get();
   272→    }
   273→
   274→    // Rebuild compact string pool
   275→    std::unordered_set<uint32_t> used_offsets;
   276→    for (const auto& w : out.ways) used_offsets.insert(w.name_id);
   277→    for (const auto& a : out.addr_points) { used_offsets.insert(a.housenumber_id); used_offsets.insert(a.street_id); }
   278→    for (const auto& iw : out.interp_ways) used_offsets.insert(iw.street_id);
   279→    for (const auto& ap : out.admin_polygons) used_offsets.insert(ap.name_id);
   280→
   281→    const auto& old_sp = full.string_pool.data();
   282→    std::unordered_map<uint32_t, uint32_t> string_remap;
   283→    auto& new_sp = out.string_pool.mutable_data();
   284→    new_sp.clear();
   285→
   286→    std::vector<uint32_t> sorted_offsets(used_offsets.begin(), used_offsets.end());
   287→    std::sort(sorted_offsets.begin(), sorted_offsets.end());
   288→    for (uint32_t old_off : sorted_offsets) {
   289→        uint32_t new_off = static_cast<uint32_t>(new_sp.size());
   290→        string_remap[old_off] = new_off;
   291→        const char* str = old_sp.data() + old_off;
   292→        size_t len = std::strlen(str);
   293→        new_sp.insert(new_sp.end(), str, str + len + 1);
   294→    }
   295→
   296→    for (auto& w : out.ways) w.name_id = string_remap[w.name_id];
   297→    for (auto& a : out.addr_points) { a.housenumber_id = string_remap[a.housenumber_id]; a.street_id = string_remap[a.street_id]; }
   298→    for (auto& iw : out.interp_ways) iw.street_id = string_remap[iw.street_id];
   299→    for (auto& ap : out.admin_polygons) ap.name_id = string_remap[ap.name_id];
   300→
   301→    return out;
   302→}

ParsedData filter_by_bbox(const ParsedData& full, const ContinentBBox& bbox) {
    ParsedData out;

    std::cerr << "  DEBUG filter_by_bbox: ways_sorted=" << full.sorted_way_cells.size()
              << " addrs_sorted=" << full.sorted_addr_cells.size()
              << " interps_sorted=" << full.sorted_interp_cells.size()
              << " ways_map=" << full.cell_to_ways.size()
              << " addrs_map=" << full.cell_to_addrs.size()
              << " interps_map=" << full.cell_to_interps.size()
              << " admin_map=" << full.cell_to_admin.size()
              << std::endl;

    // Filter from hash map
    auto filter_cells_map = [&](const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map,
                                bool mask_interior = false) {
        std::cerr << "    DEBUG filter_cells_map: size=" << cell_map.size() << std::endl;
        std::unordered_set<uint32_t> ids;
        for (const auto& [cell_id, cell_ids] : cell_map) {
            if (cell_in_bbox(cell_id, bbox)) {
                for (uint32_t id : cell_ids)
                    ids.insert(mask_interior ? (id & ID_MASK) : id);
            }
        }
        return ids;
    };

    // Filter from sorted pairs — parallel chunked scan.
    // Split the sorted array into N chunks at cell boundaries, each thread
    // builds its own ID set, then merge.
    auto filter_cells_sorted = [&](const std::vector<CellItemPair>& sorted,
                                   bool mask_interior = false) {
        if (sorted.empty()) return std::unordered_set<uint32_t>{};

        unsigned nthreads = std::max(1u, std::thread::hardware_concurrency() / 4);
        size_t chunk = (sorted.size() + nthreads - 1) / nthreads;
        std::cerr << "    DEBUG filter_cells_sorted: size=" << sorted.size()
                  << " nthreads=" << nthreads << " chunk=" << chunk << std::endl;

        // Find chunk boundaries at cell_id transitions
        std::vector<size_t> bounds = {0};
        for (unsigned t = 1; t < nthreads; t++) {
            size_t target = t * chunk;
            if (target >= sorted.size()) break;
            while (target < sorted.size() && sorted[target].cell_id == sorted[target-1].cell_id)
                target++;
            if (target < sorted.size()) bounds.push_back(target);
        }
        bounds.push_back(sorted.size());

        std::vector<std::unordered_set<uint32_t>> thread_ids(bounds.size() - 1);
        std::vector<std::thread> threads;
        for (size_t t = 0; t + 1 < bounds.size(); t++) {
            threads.emplace_back([&, t]() {
                auto& local = thread_ids[t];
                for (size_t i = bounds[t]; i < bounds[t+1]; ) {
                    uint64_t cell_id = sorted[i].cell_id;
                    bool match = cell_in_bbox(cell_id, bbox);
                    while (i < bounds[t+1] && sorted[i].cell_id == cell_id) {
                        if (match) {
                            uint32_t id = sorted[i].item_id;
                            local.insert(mask_interior ? (id & ID_MASK) : id);
                        }
                        i++;
                    }
                }
            });
        }
        std::cerr << "    DEBUG: spawned " << threads.size() << " threads, "
                  << bounds.size() - 1 << " chunks" << std::endl;
        for (auto& t : threads) t.join();

        // Merge thread-local sets
        size_t largest = 0;
        for (size_t t = 0; t < thread_ids.size(); t++)
            if (thread_ids[t].size() > thread_ids[largest].size()) largest = t;
        auto& merged = thread_ids[largest];
        for (size_t t = 0; t < thread_ids.size(); t++) {
            if (t == largest) continue;
            for (auto id : thread_ids[t]) merged.insert(id);
        }
        return std::move(merged);
    };

    // Use sorted pairs where available (faster linear scan), fall back to hash maps
    auto f_ways = std::async(std::launch::async, [&]{
        return !full.sorted_way_cells.empty()
            ? filter_cells_sorted(full.sorted_way_cells)
            : filter_cells_map(full.cell_to_ways);
    });
    auto f_addrs = std::async(std::launch::async, [&]{
        return !full.sorted_addr_cells.empty()
            ? filter_cells_sorted(full.sorted_addr_cells)
            : filter_cells_map(full.cell_to_addrs);
    });
    auto f_interps = std::async(std::launch::async, [&]{
        return !full.sorted_interp_cells.empty()
            ? filter_cells_sorted(full.sorted_interp_cells)
            : filter_cells_map(full.cell_to_interps);
    });
    auto f_admin = std::async(std::launch::async, [&]{
        return filter_cells_map(full.cell_to_admin, true);
    });

    auto used_way_ids = f_ways.get();
    auto used_addr_ids = f_addrs.get();
    auto used_interp_ids = f_interps.get();
    auto used_admin_ids = f_admin.get();

    // Remap all 4 data types in parallel — each builds its own vectors and remap table
    std::unordered_map<uint32_t, uint32_t> way_remap, addr_remap, interp_remap, admin_remap;

    // Ways
    auto f_remap_ways = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_way_ids.begin(), used_way_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<WayHeader> ways;
        std::vector<NodeCoord> nodes;
        ways.reserve(sorted_ids.size());
        nodes.reserve(sorted_ids.size() * 5);
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(ways.size());
            const auto& w = full.ways[old_id];
            WayHeader nw = w;
            nw.node_offset = static_cast<uint32_t>(nodes.size());
            ways.push_back(nw);
            for (uint8_t n = 0; n < w.node_count; n++)
                nodes.push_back(full.street_nodes[w.node_offset + n]);
        }
        return std::make_tuple(std::move(remap), std::move(ways), std::move(nodes));
    });

    // Addrs
    auto f_remap_addrs = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_addr_ids.begin(), used_addr_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<AddrPoint> addrs;
        addrs.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(addrs.size());
            addrs.push_back(full.addr_points[old_id]);
        }
        return std::make_tuple(std::move(remap), std::move(addrs));
    });

    // Interps
    auto f_remap_interps = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_interp_ids.begin(), used_interp_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<InterpWay> iways;
        std::vector<NodeCoord> inodes;
        iways.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(iways.size());
            const auto& iw = full.interp_ways[old_id];
            InterpWay niw = iw;
            niw.node_offset = static_cast<uint32_t>(inodes.size());
            iways.push_back(niw);
            for (uint8_t n = 0; n < iw.node_count; n++)
                inodes.push_back(full.interp_nodes[iw.node_offset + n]);
        }
        return std::make_tuple(std::move(remap), std::move(iways), std::move(inodes));
    });

    // Admins
    auto f_remap_admins = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_admin_ids.begin(), used_admin_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<AdminPolygon> polys;
        std::vector<NodeCoord> verts;
        polys.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(polys.size());
            const auto& ap = full.admin_polygons[old_id];
            AdminPolygon nap = ap;
            nap.vertex_offset = static_cast<uint32_t>(verts.size());
            polys.push_back(nap);
            for (uint32_t v = 0; v < ap.vertex_count; v++)
                verts.push_back(full.admin_vertices[ap.vertex_offset + v]);
        }
        return std::make_tuple(std::move(remap), std::move(polys), std::move(verts));
    });

    // Collect results
    {
        auto [wr, ways, nodes] = f_remap_ways.get();
        way_remap = std::move(wr);
        out.ways = std::move(ways);
        out.street_nodes = std::move(nodes);
    }
    {
        auto [ar, addrs] = f_remap_addrs.get();
        addr_remap = std::move(ar);
        out.addr_points = std::move(addrs);
    }
    {
        auto [ir, iways, inodes] = f_remap_interps.get();
        interp_remap = std::move(ir);
        out.interp_ways = std::move(iways);
        out.interp_nodes = std::move(inodes);
    }
    {
        auto [ar, polys, verts] = f_remap_admins.get();
        admin_remap = std::move(ar);
        out.admin_polygons = std::move(polys);
        out.admin_vertices = std::move(verts);
    }

    // Remap cell maps in parallel (each builds an independent output map)
    auto remap_cells = [&](const std::unordered_map<uint64_t, std::vector<uint32_t>>& src,
                           const std::unordered_map<uint32_t, uint32_t>& remap,
                           std::unordered_map<uint64_t, std::vector<uint32_t>>& dst,
                           bool handle_flags = false) {
        for (const auto& [cell_id, ids] : src) {
            if (!cell_in_bbox(cell_id, bbox)) continue;
            std::vector<uint32_t> new_ids;
            for (uint32_t id : ids) {
                uint32_t raw_id = handle_flags ? (id & ID_MASK) : id;
                uint32_t flags = handle_flags ? (id & INTERIOR_FLAG) : 0;
                auto it = remap.find(raw_id);
                if (it != remap.end()) new_ids.push_back(it->second | flags);
            }
            if (!new_ids.empty()) dst[cell_id] = std::move(new_ids);
        }
    };

    {
        auto f1 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_ways, way_remap, out.cell_to_ways); });
        auto f2 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_addrs, addr_remap, out.cell_to_addrs); });
        auto f3 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_interps, interp_remap, out.cell_to_interps); });
        auto f4 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_admin, admin_remap, out.cell_to_admin, true); });
        f1.get(); f2.get(); f3.get(); f4.get();
    }

    // Rebuild compact string pool
    std::unordered_set<uint32_t> used_offsets;
    for (const auto& w : out.ways) used_offsets.insert(w.name_id);
    for (const auto& a : out.addr_points) { used_offsets.insert(a.housenumber_id); used_offsets.insert(a.street_id); }
    for (const auto& iw : out.interp_ways) used_offsets.insert(iw.street_id);
    for (const auto& ap : out.admin_polygons) used_offsets.insert(ap.name_id);

    const auto& old_sp = full.string_pool.data();
    std::unordered_map<uint32_t, uint32_t> string_remap;
    auto& new_sp = out.string_pool.mutable_data();
    new_sp.clear();

    std::vector<uint32_t> sorted_offsets(used_offsets.begin(), used_offsets.end());
    std::sort(sorted_offsets.begin(), sorted_offsets.end());
    for (uint32_t old_off : sorted_offsets) {
        uint32_t new_off = static_cast<uint32_t>(new_sp.size());
        string_remap[old_off] = new_off;
        const char* str = old_sp.data() + old_off;
        size_t len = std::strlen(str);
        new_sp.insert(new_sp.end(), str, str + len + 1);
    }

    for (auto& w : out.ways) w.name_id = string_remap[w.name_id];
    for (auto& a : out.addr_points) { a.housenumber_id = string_remap[a.housenumber_id]; a.street_id = string_remap[a.street_id]; }
    for (auto& iw : out.interp_ways) iw.street_id = string_remap[iw.street_id];
    for (auto& ap : out.admin_polygons) ap.name_id = string_remap[ap.name_id];

    return out;
}


Now I can see the full function. Let me add timing to every sub-phase. I'll replace the debug output with proper timing:


Edit

ParsedData filter_by_bbox(const ParsedData& full, const ContinentBBox& bbox) {
    ParsedData out;

    std::cerr << "  DEBUG filter_by_bbox: ways_sorted=" << full.sorted_way_cells.size()
              << " addrs_sorted=" << full.sorted_addr_cells.size()
              << " interps_sorted=" << full.sorted_interp_cells.size()
              << " ways_map=" << full.cell_to_ways.size()
              << " addrs_map=" << full.cell_to_addrs.size()
              << " interps_map=" << full.cell_to_interps.size()
              << " admin_map=" << full.cell_to_admin.size()
              << std::endl;

ParsedData filter_by_bbox(const ParsedData& full, const ContinentBBox& bbox) {
    ParsedData out;
    auto _ft = std::chrono::steady_clock::now();
    auto _fc = CpuTicks::now();

    std::cerr << "    data: ways_sorted=" << full.sorted_way_cells.size()
              << " addrs_sorted=" << full.sorted_addr_cells.size()
              << " interps_sorted=" << full.sorted_interp_cells.size()
              << " ways_map=" << full.cell_to_ways.size()
              << " addrs_map=" << full.cell_to_addrs.size()
              << " interps_map=" << full.cell_to_interps.size()
              << " admin_map=" << full.cell_to_admin.size()
              << std::endl;


The file /root/traccar-geocoder/builder/src/continent_filter.cpp has been updated successfully.

ParsedData filter_by_bbox(const ParsedData& full, const ContinentBBox& bbox) {
    ParsedData out;
    auto _ft = std::chrono::steady_clock::now();
    auto _fc = CpuTicks::now();

    std::cerr << "    data: ways_sorted=" << full.sorted_way_cells.size()
              << " addrs_sorted=" << full.sorted_addr_cells.size()
              << " interps_sorted=" << full.sorted_interp_cells.size()
              << " ways_map=" << full.cell_to_ways.size()
              << " addrs_map=" << full.cell_to_addrs.size()
              << " interps_map=" << full.cell_to_interps.size()
              << " admin_map=" << full.cell_to_admin.size()
              << std::endl;

#include "continent_filter.h"

#include <algorithm>
#include <cstring>
#include <future>
#include <unordered_set>

#include <s2/s2cell_id.h>
#include <s2/s2latlng.h>

const ContinentBBox kContinents[] = {
    {"africa",            -35.0,  37.5,  -25.0,  55.0},
    {"asia",              -12.0,  82.0,   25.0, 180.0},
    {"europe",             35.0,  72.0,  -25.0,  45.0},
    {"north-america",       7.0,  84.0, -170.0, -50.0},
    {"south-america",     -56.0,  13.0,  -82.0, -34.0},
    {"oceania",           -50.0,   0.0,  110.0, 180.0},
    {"central-america",     7.0,  23.5, -120.0, -57.0},
    {"antarctica",        -90.0, -60.0, -180.0, 180.0},
};

const size_t kContinentCount = sizeof(kContinents) / sizeof(kContinents[0]);

static bool cell_in_bbox(uint64_t cell_id, const ContinentBBox& bbox) {
    S2CellId cell(cell_id);
    S2LatLng center = cell.ToLatLng();
    double lat = center.lat().degrees();
    double lng = center.lng().degrees();
    return lat >= bbox.min_lat && lat <= bbox.max_lat &&
           lng >= bbox.min_lng && lng <= bbox.max_lng;
}

ParsedData filter_by_bbox(const ParsedData& full, const ContinentBBox& bbox) {
    ParsedData out;

    std::cerr << "  DEBUG filter_by_bbox: ways_sorted=" << full.sorted_way_cells.size()
              << " addrs_sorted=" << full.sorted_addr_cells.size()
              << " interps_sorted=" << full.sorted_interp_cells.size()
              << " ways_map=" << full.cell_to_ways.size()
              << " addrs_map=" << full.cell_to_addrs.size()
              << " interps_map=" << full.cell_to_interps.size()
              << " admin_map=" << full.cell_to_admin.size()
              << std::endl;

    // Filter from hash map
    auto filter_cells_map = [&](const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map,
                                bool mask_interior = false) {
        std::cerr << "    DEBUG filter_cells_map: size=" << cell_map.size() << std::endl;
        std::unordered_set<uint32_t> ids;
        for (const auto& [cell_id, cell_ids] : cell_map) {
            if (cell_in_bbox(cell_id, bbox)) {
                for (uint32_t id : cell_ids)
                    ids.insert(mask_interior ? (id & ID_MASK) : id);
            }
        }
        return ids;
    };

    // Filter from sorted pairs — parallel chunked scan.
    // Split the sorted array into N chunks at cell boundaries, each thread
    // builds its own ID set, then merge.
    auto filter_cells_sorted = [&](const std::vector<CellItemPair>& sorted,
                                   bool mask_interior = false) {
        if (sorted.empty()) return std::unordered_set<uint32_t>{};

        unsigned nthreads = std::max(1u, std::thread::hardware_concurrency() / 4);
        size_t chunk = (sorted.size() + nthreads - 1) / nthreads;
        std::cerr << "    DEBUG filter_cells_sorted: size=" << sorted.size()
                  << " nthreads=" << nthreads << " chunk=" << chunk << std::endl;

        // Find chunk boundaries at cell_id transitions
        std::vector<size_t> bounds = {0};
        for (unsigned t = 1; t < nthreads; t++) {
            size_t target = t * chunk;
            if (target >= sorted.size()) break;
            while (target < sorted.size() && sorted[target].cell_id == sorted[target-1].cell_id)
                target++;
            if (target < sorted.size()) bounds.push_back(target);
        }
        bounds.push_back(sorted.size());

        std::vector<std::unordered_set<uint32_t>> thread_ids(bounds.size() - 1);
        std::vector<std::thread> threads;
        for (size_t t = 0; t + 1 < bounds.size(); t++) {
            threads.emplace_back([&, t]() {
                auto& local = thread_ids[t];
                for (size_t i = bounds[t]; i < bounds[t+1]; ) {
                    uint64_t cell_id = sorted[i].cell_id;
                    bool match = cell_in_bbox(cell_id, bbox);
                    while (i < bounds[t+1] && sorted[i].cell_id == cell_id) {
                        if (match) {
                            uint32_t id = sorted[i].item_id;
                            local.insert(mask_interior ? (id & ID_MASK) : id);
                        }
                        i++;
                    }
                }
            });
        }
        std::cerr << "    DEBUG: spawned " << threads.size() << " threads, "
                  << bounds.size() - 1 << " chunks" << std::endl;
        for (auto& t : threads) t.join();

        // Merge thread-local sets
        size_t largest = 0;
        for (size_t t = 0; t < thread_ids.size(); t++)
            if (thread_ids[t].size() > thread_ids[largest].size()) largest = t;
        auto& merged = thread_ids[largest];
        for (size_t t = 0; t < thread_ids.size(); t++) {
            if (t == largest) continue;
            for (auto id : thread_ids[t]) merged.insert(id);
        }
        return std::move(merged);
    };

    // Use sorted pairs where available (faster linear scan), fall back to hash maps
    auto f_ways = std::async(std::launch::async, [&]{
        return !full.sorted_way_cells.empty()
            ? filter_cells_sorted(full.sorted_way_cells)
            : filter_cells_map(full.cell_to_ways);
    });
    auto f_addrs = std::async(std::launch::async, [&]{
        return !full.sorted_addr_cells.empty()
            ? filter_cells_sorted(full.sorted_addr_cells)
            : filter_cells_map(full.cell_to_addrs);
    });
    auto f_interps = std::async(std::launch::async, [&]{
        return !full.sorted_interp_cells.empty()
            ? filter_cells_sorted(full.sorted_interp_cells)
            : filter_cells_map(full.cell_to_interps);
    });
    auto f_admin = std::async(std::launch::async, [&]{
        return filter_cells_map(full.cell_to_admin, true);
    });

    auto used_way_ids = f_ways.get();
    auto used_addr_ids = f_addrs.get();
    auto used_interp_ids = f_interps.get();
    auto used_admin_ids = f_admin.get();

    // Remap all 4 data types in parallel — each builds its own vectors and remap table
    std::unordered_map<uint32_t, uint32_t> way_remap, addr_remap, interp_remap, admin_remap;

    // Ways
    auto f_remap_ways = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_way_ids.begin(), used_way_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<WayHeader> ways;
        std::vector<NodeCoord> nodes;
        ways.reserve(sorted_ids.size());
        nodes.reserve(sorted_ids.size() * 5);
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(ways.size());
            const auto& w = full.ways[old_id];
            WayHeader nw = w;
            nw.node_offset = static_cast<uint32_t>(nodes.size());
            ways.push_back(nw);
            for (uint8_t n = 0; n < w.node_count; n++)
                nodes.push_back(full.street_nodes[w.node_offset + n]);
        }
        return std::make_tuple(std::move(remap), std::move(ways), std::move(nodes));
    });

    // Addrs
    auto f_remap_addrs = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_addr_ids.begin(), used_addr_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<AddrPoint> addrs;
        addrs.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(addrs.size());
            addrs.push_back(full.addr_points[old_id]);
        }
        return std::make_tuple(std::move(remap), std::move(addrs));
    });

    // Interps
    auto f_remap_interps = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_interp_ids.begin(), used_interp_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<InterpWay> iways;
        std::vector<NodeCoord> inodes;
        iways.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(iways.size());
            const auto& iw = full.interp_ways[old_id];
            InterpWay niw = iw;
            niw.node_offset = static_cast<uint32_t>(inodes.size());
            iways.push_back(niw);
            for (uint8_t n = 0; n < iw.node_count; n++)
                inodes.push_back(full.interp_nodes[iw.node_offset + n]);
        }
        return std::make_tuple(std::move(remap), std::move(iways), std::move(inodes));
    });

    // Admins
    auto f_remap_admins = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_admin_ids.begin(), used_admin_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<AdminPolygon> polys;
        std::vector<NodeCoord> verts;
        polys.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(polys.size());
            const auto& ap = full.admin_polygons[old_id];
            AdminPolygon nap = ap;
            nap.vertex_offset = static_cast<uint32_t>(verts.size());
            polys.push_back(nap);
            for (uint32_t v = 0; v < ap.vertex_count; v++)
                verts.push_back(full.admin_vertices[ap.vertex_offset + v]);
        }
        return std::make_tuple(std::move(remap), std::move(polys), std::move(verts));
    });

    // Collect results
    {
        auto [wr, ways, nodes] = f_remap_ways.get();
        way_remap = std::move(wr);
        out.ways = std::move(ways);
        out.street_nodes = std::move(nodes);
    }
    {
        auto [ar, addrs] = f_remap_addrs.get();
        addr_remap = std::move(ar);
        out.addr_points = std::move(addrs);
    }
    {
        auto [ir, iways, inodes] = f_remap_interps.get();
        interp_remap = std::move(ir);
        out.interp_ways = std::move(iways);
        out.interp_nodes = std::move(inodes);
    }
    {
        auto [ar, polys, verts] = f_remap_admins.get();
        admin_remap = std::move(ar);
        out.admin_polygons = std::move(polys);
        out.admin_vertices = std::move(verts);
    }

    // Remap cell maps in parallel (each builds an independent output map)
    auto remap_cells = [&](const std::unordered_map<uint64_t, std::vector<uint32_t>>& src,
                           const std::unordered_map<uint32_t, uint32_t>& remap,
                           std::unordered_map<uint64_t, std::vector<uint32_t>>& dst,
                           bool handle_flags = false) {
        for (const auto& [cell_id, ids] : src) {
            if (!cell_in_bbox(cell_id, bbox)) continue;
            std::vector<uint32_t> new_ids;
            for (uint32_t id : ids) {
                uint32_t raw_id = handle_flags ? (id & ID_MASK) : id;
                uint32_t flags = handle_flags ? (id & INTERIOR_FLAG) : 0;
                auto it = remap.find(raw_id);
                if (it != remap.end()) new_ids.push_back(it->second | flags);
            }
            if (!new_ids.empty()) dst[cell_id] = std::move(new_ids);
        }
    };

    {
        auto f1 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_ways, way_remap, out.cell_to_ways); });
        auto f2 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_addrs, addr_remap, out.cell_to_addrs); });
        auto f3 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_interps, interp_remap, out.cell_to_interps); });
        auto f4 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_admin, admin_remap, out.cell_to_admin, true); });
        f1.get(); f2.get(); f3.get(); f4.get();
    }

    // Rebuild compact string pool
    std::unordered_set<uint32_t> used_offsets;
    for (const auto& w : out.ways) used_offsets.insert(w.name_id);
    for (const auto& a : out.addr_points) { used_offsets.insert(a.housenumber_id); used_offsets.insert(a.street_id); }
    for (const auto& iw : out.interp_ways) used_offsets.insert(iw.street_id);
    for (const auto& ap : out.admin_polygons) used_offsets.insert(ap.name_id);

    const auto& old_sp = full.string_pool.data();
    std::unordered_map<uint32_t, uint32_t> string_remap;
    auto& new_sp = out.string_pool.mutable_data();
    new_sp.clear();

    std::vector<uint32_t> sorted_offsets(used_offsets.begin(), used_offsets.end());
    std::sort(sorted_offsets.begin(), sorted_offsets.end());
    for (uint32_t old_off : sorted_offsets) {
        uint32_t new_off = static_cast<uint32_t>(new_sp.size());
        string_remap[old_off] = new_off;
        const char* str = old_sp.data() + old_off;
        size_t len = std::strlen(str);
        new_sp.insert(new_sp.end(), str, str + len + 1);
    }

    for (auto& w : out.ways) w.name_id = string_remap[w.name_id];
    for (auto& a : out.addr_points) { a.housenumber_id = string_remap[a.housenumber_id]; a.street_id = string_remap[a.street_id]; }
    for (auto& iw : out.interp_ways) iw.street_id = string_remap[iw.street_id];
    for (auto& ap : out.admin_polygons) ap.name_id = string_remap[ap.name_id];

    return out;
}

+    auto _ft = std::chrono::steady_clock::now();

-    std::cerr << "  DEBUG filter_by_bbox: ways_sorted=" << full.sorted_way_cells.size()

+    std::cerr << "    data: ways_sorted=" << full.sorted_way_cells.size()


Now add timing after each major sub-phase. First after the ID collection:


    auto used_way_ids = f_ways.get();
    auto used_addr_ids = f_addrs.get();
    auto used_interp_ids = f_interps.get();
    auto used_admin_ids = f_admin.get();

    // Remap all 4 data types in parallel

    auto used_way_ids = f_ways.get();
    auto used_addr_ids = f_addrs.get();
    auto used_interp_ids = f_interps.get();
    auto used_admin_ids = f_admin.get();
    log_phase("      filter: ID collection", _ft, _fc);

    // Remap all 4 data types in parallel


    auto used_way_ids = f_ways.get();
    auto used_addr_ids = f_addrs.get();
    auto used_interp_ids = f_interps.get();
    auto used_admin_ids = f_admin.get();
    log_phase("      filter: ID collection", _ft, _fc);

    // Remap all 4 data types in parallel

#include "continent_filter.h"

#include <algorithm>
#include <cstring>
#include <future>
#include <unordered_set>

#include <s2/s2cell_id.h>
#include <s2/s2latlng.h>

const ContinentBBox kContinents[] = {
    {"africa",            -35.0,  37.5,  -25.0,  55.0},
    {"asia",              -12.0,  82.0,   25.0, 180.0},
    {"europe",             35.0,  72.0,  -25.0,  45.0},
    {"north-america",       7.0,  84.0, -170.0, -50.0},
    {"south-america",     -56.0,  13.0,  -82.0, -34.0},
    {"oceania",           -50.0,   0.0,  110.0, 180.0},
    {"central-america",     7.0,  23.5, -120.0, -57.0},
    {"antarctica",        -90.0, -60.0, -180.0, 180.0},
};

const size_t kContinentCount = sizeof(kContinents) / sizeof(kContinents[0]);

static bool cell_in_bbox(uint64_t cell_id, const ContinentBBox& bbox) {
    S2CellId cell(cell_id);
    S2LatLng center = cell.ToLatLng();
    double lat = center.lat().degrees();
    double lng = center.lng().degrees();
    return lat >= bbox.min_lat && lat <= bbox.max_lat &&
           lng >= bbox.min_lng && lng <= bbox.max_lng;
}

ParsedData filter_by_bbox(const ParsedData& full, const ContinentBBox& bbox) {
    ParsedData out;
    auto _ft = std::chrono::steady_clock::now();
    auto _fc = CpuTicks::now();

    std::cerr << "    data: ways_sorted=" << full.sorted_way_cells.size()
              << " addrs_sorted=" << full.sorted_addr_cells.size()
              << " interps_sorted=" << full.sorted_interp_cells.size()
              << " ways_map=" << full.cell_to_ways.size()
              << " addrs_map=" << full.cell_to_addrs.size()
              << " interps_map=" << full.cell_to_interps.size()
              << " admin_map=" << full.cell_to_admin.size()
              << std::endl;

    // Filter from hash map
    auto filter_cells_map = [&](const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map,
                                bool mask_interior = false) {
        std::cerr << "    DEBUG filter_cells_map: size=" << cell_map.size() << std::endl;
        std::unordered_set<uint32_t> ids;
        for (const auto& [cell_id, cell_ids] : cell_map) {
            if (cell_in_bbox(cell_id, bbox)) {
                for (uint32_t id : cell_ids)
                    ids.insert(mask_interior ? (id & ID_MASK) : id);
            }
        }
        return ids;
    };

    // Filter from sorted pairs — parallel chunked scan.
    // Split the sorted array into N chunks at cell boundaries, each thread
    // builds its own ID set, then merge.
    auto filter_cells_sorted = [&](const std::vector<CellItemPair>& sorted,
                                   bool mask_interior = false) {
        if (sorted.empty()) return std::unordered_set<uint32_t>{};

        unsigned nthreads = std::max(1u, std::thread::hardware_concurrency() / 4);
        size_t chunk = (sorted.size() + nthreads - 1) / nthreads;
        std::cerr << "    DEBUG filter_cells_sorted: size=" << sorted.size()
                  << " nthreads=" << nthreads << " chunk=" << chunk << std::endl;

        // Find chunk boundaries at cell_id transitions
        std::vector<size_t> bounds = {0};
        for (unsigned t = 1; t < nthreads; t++) {
            size_t target = t * chunk;
            if (target >= sorted.size()) break;
            while (target < sorted.size() && sorted[target].cell_id == sorted[target-1].cell_id)
                target++;
            if (target < sorted.size()) bounds.push_back(target);
        }
        bounds.push_back(sorted.size());

        std::vector<std::unordered_set<uint32_t>> thread_ids(bounds.size() - 1);
        std::vector<std::thread> threads;
        for (size_t t = 0; t + 1 < bounds.size(); t++) {
            threads.emplace_back([&, t]() {
                auto& local = thread_ids[t];
                for (size_t i = bounds[t]; i < bounds[t+1]; ) {
                    uint64_t cell_id = sorted[i].cell_id;
                    bool match = cell_in_bbox(cell_id, bbox);
                    while (i < bounds[t+1] && sorted[i].cell_id == cell_id) {
                        if (match) {
                            uint32_t id = sorted[i].item_id;
                            local.insert(mask_interior ? (id & ID_MASK) : id);
                        }
                        i++;
                    }
                }
            });
        }
        std::cerr << "    DEBUG: spawned " << threads.size() << " threads, "
                  << bounds.size() - 1 << " chunks" << std::endl;
        for (auto& t : threads) t.join();

        // Merge thread-local sets
        size_t largest = 0;
        for (size_t t = 0; t < thread_ids.size(); t++)
            if (thread_ids[t].size() > thread_ids[largest].size()) largest = t;
        auto& merged = thread_ids[largest];
        for (size_t t = 0; t < thread_ids.size(); t++) {
            if (t == largest) continue;
            for (auto id : thread_ids[t]) merged.insert(id);
        }
        return std::move(merged);
    };

    // Use sorted pairs where available (faster linear scan), fall back to hash maps
    auto f_ways = std::async(std::launch::async, [&]{
        return !full.sorted_way_cells.empty()
            ? filter_cells_sorted(full.sorted_way_cells)
            : filter_cells_map(full.cell_to_ways);
    });
    auto f_addrs = std::async(std::launch::async, [&]{
        return !full.sorted_addr_cells.empty()
            ? filter_cells_sorted(full.sorted_addr_cells)
            : filter_cells_map(full.cell_to_addrs);
    });
    auto f_interps = std::async(std::launch::async, [&]{
        return !full.sorted_interp_cells.empty()
            ? filter_cells_sorted(full.sorted_interp_cells)
            : filter_cells_map(full.cell_to_interps);
    });
    auto f_admin = std::async(std::launch::async, [&]{
        return filter_cells_map(full.cell_to_admin, true);
    });

    auto used_way_ids = f_ways.get();
    auto used_addr_ids = f_addrs.get();
    auto used_interp_ids = f_interps.get();
    auto used_admin_ids = f_admin.get();

    // Remap all 4 data types in parallel — each builds its own vectors and remap table
    std::unordered_map<uint32_t, uint32_t> way_remap, addr_remap, interp_remap, admin_remap;

    // Ways
    auto f_remap_ways = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_way_ids.begin(), used_way_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<WayHeader> ways;
        std::vector<NodeCoord> nodes;
        ways.reserve(sorted_ids.size());
        nodes.reserve(sorted_ids.size() * 5);
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(ways.size());
            const auto& w = full.ways[old_id];
            WayHeader nw = w;
            nw.node_offset = static_cast<uint32_t>(nodes.size());
            ways.push_back(nw);
            for (uint8_t n = 0; n < w.node_count; n++)
                nodes.push_back(full.street_nodes[w.node_offset + n]);
        }
        return std::make_tuple(std::move(remap), std::move(ways), std::move(nodes));
    });

    // Addrs
    auto f_remap_addrs = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_addr_ids.begin(), used_addr_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<AddrPoint> addrs;
        addrs.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(addrs.size());
            addrs.push_back(full.addr_points[old_id]);
        }
        return std::make_tuple(std::move(remap), std::move(addrs));
    });

    // Interps
    auto f_remap_interps = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_interp_ids.begin(), used_interp_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<InterpWay> iways;
        std::vector<NodeCoord> inodes;
        iways.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(iways.size());
            const auto& iw = full.interp_ways[old_id];
            InterpWay niw = iw;
            niw.node_offset = static_cast<uint32_t>(inodes.size());
            iways.push_back(niw);
            for (uint8_t n = 0; n < iw.node_count; n++)
                inodes.push_back(full.interp_nodes[iw.node_offset + n]);
        }
        return std::make_tuple(std::move(remap), std::move(iways), std::move(inodes));
    });

    // Admins
    auto f_remap_admins = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_admin_ids.begin(), used_admin_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<AdminPolygon> polys;
        std::vector<NodeCoord> verts;
        polys.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(polys.size());
            const auto& ap = full.admin_polygons[old_id];
            AdminPolygon nap = ap;
            nap.vertex_offset = static_cast<uint32_t>(verts.size());
            polys.push_back(nap);
            for (uint32_t v = 0; v < ap.vertex_count; v++)
                verts.push_back(full.admin_vertices[ap.vertex_offset + v]);
        }
        return std::make_tuple(std::move(remap), std::move(polys), std::move(verts));
    });

    // Collect results
    {
        auto [wr, ways, nodes] = f_remap_ways.get();
        way_remap = std::move(wr);
        out.ways = std::move(ways);
        out.street_nodes = std::move(nodes);
    }
    {
        auto [ar, addrs] = f_remap_addrs.get();
        addr_remap = std::move(ar);
        out.addr_points = std::move(addrs);
    }
    {
        auto [ir, iways, inodes] = f_remap_interps.get();
        interp_remap = std::move(ir);
        out.interp_ways = std::move(iways);
        out.interp_nodes = std::move(inodes);
    }
    {
        auto [ar, polys, verts] = f_remap_admins.get();
        admin_remap = std::move(ar);
        out.admin_polygons = std::move(polys);
        out.admin_vertices = std::move(verts);
    }

    // Remap cell maps in parallel (each builds an independent output map)
    auto remap_cells = [&](const std::unordered_map<uint64_t, std::vector<uint32_t>>& src,
                           const std::unordered_map<uint32_t, uint32_t>& remap,
                           std::unordered_map<uint64_t, std::vector<uint32_t>>& dst,
                           bool handle_flags = false) {
        for (const auto& [cell_id, ids] : src) {
            if (!cell_in_bbox(cell_id, bbox)) continue;
            std::vector<uint32_t> new_ids;
            for (uint32_t id : ids) {
                uint32_t raw_id = handle_flags ? (id & ID_MASK) : id;
                uint32_t flags = handle_flags ? (id & INTERIOR_FLAG) : 0;
                auto it = remap.find(raw_id);
                if (it != remap.end()) new_ids.push_back(it->second | flags);
            }
            if (!new_ids.empty()) dst[cell_id] = std::move(new_ids);
        }
    };

    {
        auto f1 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_ways, way_remap, out.cell_to_ways); });
        auto f2 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_addrs, addr_remap, out.cell_to_addrs); });
        auto f3 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_interps, interp_remap, out.cell_to_interps); });
        auto f4 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_admin, admin_remap, out.cell_to_admin, true); });
        f1.get(); f2.get(); f3.get(); f4.get();
    }

    // Rebuild compact string pool
    std::unordered_set<uint32_t> used_offsets;
    for (const auto& w : out.ways) used_offsets.insert(w.name_id);
    for (const auto& a : out.addr_points) { used_offsets.insert(a.housenumber_id); used_offsets.insert(a.street_id); }
    for (const auto& iw : out.interp_ways) used_offsets.insert(iw.street_id);
    for (const auto& ap : out.admin_polygons) used_offsets.insert(ap.name_id);

    const auto& old_sp = full.string_pool.data();
    std::unordered_map<uint32_t, uint32_t> string_remap;
    auto& new_sp = out.string_pool.mutable_data();
    new_sp.clear();

    std::vector<uint32_t> sorted_offsets(used_offsets.begin(), used_offsets.end());
    std::sort(sorted_offsets.begin(), sorted_offsets.end());
    for (uint32_t old_off : sorted_offsets) {
        uint32_t new_off = static_cast<uint32_t>(new_sp.size());
        string_remap[old_off] = new_off;
        const char* str = old_sp.data() + old_off;
        size_t len = std::strlen(str);
        new_sp.insert(new_sp.end(), str, str + len + 1);
    }

    for (auto& w : out.ways) w.name_id = string_remap[w.name_id];
    for (auto& a : out.addr_points) { a.housenumber_id = string_remap[a.housenumber_id]; a.street_id = string_remap[a.street_id]; }
    for (auto& iw : out.interp_ways) iw.street_id = string_remap[iw.street_id];
    for (auto& ap : out.admin_polygons) ap.name_id = string_remap[ap.name_id];

    return out;
}

+    log_phase("      filter: ID collection", _ft, _fc);


After the remap collect:


    {
        auto [ar, polys, verts] = f_remap_admins.get();
        admin_remap = std::move(ar);
        out.admin_polygons = std::move(polys);
        out.admin_vertices = std::move(verts);
    }

    // Remap cell maps in parallel (each builds an independent output map)

    {
        auto [ar, polys, verts] = f_remap_admins.get();
        admin_remap = std::move(ar);
        out.admin_polygons = std::move(polys);
        out.admin_vertices = std::move(verts);
    }
    log_phase("      filter: data remap", _ft, _fc);

    // Remap cell maps in parallel (each builds an independent output map)


    {
        auto [ar, polys, verts] = f_remap_admins.get();
        admin_remap = std::move(ar);
        out.admin_polygons = std::move(polys);
        out.admin_vertices = std::move(verts);
    }
    log_phase("      filter: data remap", _ft, _fc);

    // Remap cell maps in parallel (each builds an independent output map)

#include "continent_filter.h"

#include <algorithm>
#include <cstring>
#include <future>
#include <unordered_set>

#include <s2/s2cell_id.h>
#include <s2/s2latlng.h>

const ContinentBBox kContinents[] = {
    {"africa",            -35.0,  37.5,  -25.0,  55.0},
    {"asia",              -12.0,  82.0,   25.0, 180.0},
    {"europe",             35.0,  72.0,  -25.0,  45.0},
    {"north-america",       7.0,  84.0, -170.0, -50.0},
    {"south-america",     -56.0,  13.0,  -82.0, -34.0},
    {"oceania",           -50.0,   0.0,  110.0, 180.0},
    {"central-america",     7.0,  23.5, -120.0, -57.0},
    {"antarctica",        -90.0, -60.0, -180.0, 180.0},
};

const size_t kContinentCount = sizeof(kContinents) / sizeof(kContinents[0]);

static bool cell_in_bbox(uint64_t cell_id, const ContinentBBox& bbox) {
    S2CellId cell(cell_id);
    S2LatLng center = cell.ToLatLng();
    double lat = center.lat().degrees();
    double lng = center.lng().degrees();
    return lat >= bbox.min_lat && lat <= bbox.max_lat &&
           lng >= bbox.min_lng && lng <= bbox.max_lng;
}

ParsedData filter_by_bbox(const ParsedData& full, const ContinentBBox& bbox) {
    ParsedData out;
    auto _ft = std::chrono::steady_clock::now();
    auto _fc = CpuTicks::now();

    std::cerr << "    data: ways_sorted=" << full.sorted_way_cells.size()
              << " addrs_sorted=" << full.sorted_addr_cells.size()
              << " interps_sorted=" << full.sorted_interp_cells.size()
              << " ways_map=" << full.cell_to_ways.size()
              << " addrs_map=" << full.cell_to_addrs.size()
              << " interps_map=" << full.cell_to_interps.size()
              << " admin_map=" << full.cell_to_admin.size()
              << std::endl;

    // Filter from hash map
    auto filter_cells_map = [&](const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map,
                                bool mask_interior = false) {
        std::cerr << "    DEBUG filter_cells_map: size=" << cell_map.size() << std::endl;
        std::unordered_set<uint32_t> ids;
        for (const auto& [cell_id, cell_ids] : cell_map) {
            if (cell_in_bbox(cell_id, bbox)) {
                for (uint32_t id : cell_ids)
                    ids.insert(mask_interior ? (id & ID_MASK) : id);
            }
        }
        return ids;
    };

    // Filter from sorted pairs — parallel chunked scan.
    // Split the sorted array into N chunks at cell boundaries, each thread
    // builds its own ID set, then merge.
    auto filter_cells_sorted = [&](const std::vector<CellItemPair>& sorted,
                                   bool mask_interior = false) {
        if (sorted.empty()) return std::unordered_set<uint32_t>{};

        unsigned nthreads = std::max(1u, std::thread::hardware_concurrency() / 4);
        size_t chunk = (sorted.size() + nthreads - 1) / nthreads;
        std::cerr << "    DEBUG filter_cells_sorted: size=" << sorted.size()
                  << " nthreads=" << nthreads << " chunk=" << chunk << std::endl;

        // Find chunk boundaries at cell_id transitions
        std::vector<size_t> bounds = {0};
        for (unsigned t = 1; t < nthreads; t++) {
            size_t target = t * chunk;
            if (target >= sorted.size()) break;
            while (target < sorted.size() && sorted[target].cell_id == sorted[target-1].cell_id)
                target++;
            if (target < sorted.size()) bounds.push_back(target);
        }
        bounds.push_back(sorted.size());

        std::vector<std::unordered_set<uint32_t>> thread_ids(bounds.size() - 1);
        std::vector<std::thread> threads;
        for (size_t t = 0; t + 1 < bounds.size(); t++) {
            threads.emplace_back([&, t]() {
                auto& local = thread_ids[t];
                for (size_t i = bounds[t]; i < bounds[t+1]; ) {
                    uint64_t cell_id = sorted[i].cell_id;
                    bool match = cell_in_bbox(cell_id, bbox);
                    while (i < bounds[t+1] && sorted[i].cell_id == cell_id) {
                        if (match) {
                            uint32_t id = sorted[i].item_id;
                            local.insert(mask_interior ? (id & ID_MASK) : id);
                        }
                        i++;
                    }
                }
            });
        }
        std::cerr << "    DEBUG: spawned " << threads.size() << " threads, "
                  << bounds.size() - 1 << " chunks" << std::endl;
        for (auto& t : threads) t.join();

        // Merge thread-local sets
        size_t largest = 0;
        for (size_t t = 0; t < thread_ids.size(); t++)
            if (thread_ids[t].size() > thread_ids[largest].size()) largest = t;
        auto& merged = thread_ids[largest];
        for (size_t t = 0; t < thread_ids.size(); t++) {
            if (t == largest) continue;
            for (auto id : thread_ids[t]) merged.insert(id);
        }
        return std::move(merged);
    };

    // Use sorted pairs where available (faster linear scan), fall back to hash maps
    auto f_ways = std::async(std::launch::async, [&]{
        return !full.sorted_way_cells.empty()
            ? filter_cells_sorted(full.sorted_way_cells)
            : filter_cells_map(full.cell_to_ways);
    });
    auto f_addrs = std::async(std::launch::async, [&]{
        return !full.sorted_addr_cells.empty()
            ? filter_cells_sorted(full.sorted_addr_cells)
            : filter_cells_map(full.cell_to_addrs);
    });
    auto f_interps = std::async(std::launch::async, [&]{
        return !full.sorted_interp_cells.empty()
            ? filter_cells_sorted(full.sorted_interp_cells)
            : filter_cells_map(full.cell_to_interps);
    });
    auto f_admin = std::async(std::launch::async, [&]{
        return filter_cells_map(full.cell_to_admin, true);
    });

    auto used_way_ids = f_ways.get();
    auto used_addr_ids = f_addrs.get();
    auto used_interp_ids = f_interps.get();
    auto used_admin_ids = f_admin.get();
    log_phase("      filter: ID collection", _ft, _fc);

    // Remap all 4 data types in parallel — each builds its own vectors and remap table
    std::unordered_map<uint32_t, uint32_t> way_remap, addr_remap, interp_remap, admin_remap;

    // Ways
    auto f_remap_ways = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_way_ids.begin(), used_way_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<WayHeader> ways;
        std::vector<NodeCoord> nodes;
        ways.reserve(sorted_ids.size());
        nodes.reserve(sorted_ids.size() * 5);
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(ways.size());
            const auto& w = full.ways[old_id];
            WayHeader nw = w;
            nw.node_offset = static_cast<uint32_t>(nodes.size());
            ways.push_back(nw);
            for (uint8_t n = 0; n < w.node_count; n++)
                nodes.push_back(full.street_nodes[w.node_offset + n]);
        }
        return std::make_tuple(std::move(remap), std::move(ways), std::move(nodes));
    });

    // Addrs
    auto f_remap_addrs = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_addr_ids.begin(), used_addr_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<AddrPoint> addrs;
        addrs.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(addrs.size());
            addrs.push_back(full.addr_points[old_id]);
        }
        return std::make_tuple(std::move(remap), std::move(addrs));
    });

    // Interps
    auto f_remap_interps = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_interp_ids.begin(), used_interp_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<InterpWay> iways;
        std::vector<NodeCoord> inodes;
        iways.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(iways.size());
            const auto& iw = full.interp_ways[old_id];
            InterpWay niw = iw;
            niw.node_offset = static_cast<uint32_t>(inodes.size());
            iways.push_back(niw);
            for (uint8_t n = 0; n < iw.node_count; n++)
                inodes.push_back(full.interp_nodes[iw.node_offset + n]);
        }
        return std::make_tuple(std::move(remap), std::move(iways), std::move(inodes));
    });

    // Admins
    auto f_remap_admins = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_admin_ids.begin(), used_admin_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<AdminPolygon> polys;
        std::vector<NodeCoord> verts;
        polys.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(polys.size());
            const auto& ap = full.admin_polygons[old_id];
            AdminPolygon nap = ap;
            nap.vertex_offset = static_cast<uint32_t>(verts.size());
            polys.push_back(nap);
            for (uint32_t v = 0; v < ap.vertex_count; v++)
                verts.push_back(full.admin_vertices[ap.vertex_offset + v]);
        }
        return std::make_tuple(std::move(remap), std::move(polys), std::move(verts));
    });

    // Collect results
    {
        auto [wr, ways, nodes] = f_remap_ways.get();
        way_remap = std::move(wr);
        out.ways = std::move(ways);
        out.street_nodes = std::move(nodes);
    }
    {
        auto [ar, addrs] = f_remap_addrs.get();
        addr_remap = std::move(ar);
        out.addr_points = std::move(addrs);
    }
    {
        auto [ir, iways, inodes] = f_remap_interps.get();
        interp_remap = std::move(ir);
        out.interp_ways = std::move(iways);
        out.interp_nodes = std::move(inodes);
    }
    {
        auto [ar, polys, verts] = f_remap_admins.get();
        admin_remap = std::move(ar);
        out.admin_polygons = std::move(polys);
        out.admin_vertices = std::move(verts);
    }

    // Remap cell maps in parallel (each builds an independent output map)
    auto remap_cells = [&](const std::unordered_map<uint64_t, std::vector<uint32_t>>& src,
                           const std::unordered_map<uint32_t, uint32_t>& remap,
                           std::unordered_map<uint64_t, std::vector<uint32_t>>& dst,
                           bool handle_flags = false) {
        for (const auto& [cell_id, ids] : src) {
            if (!cell_in_bbox(cell_id, bbox)) continue;
            std::vector<uint32_t> new_ids;
            for (uint32_t id : ids) {
                uint32_t raw_id = handle_flags ? (id & ID_MASK) : id;
                uint32_t flags = handle_flags ? (id & INTERIOR_FLAG) : 0;
                auto it = remap.find(raw_id);
                if (it != remap.end()) new_ids.push_back(it->second | flags);
            }
            if (!new_ids.empty()) dst[cell_id] = std::move(new_ids);
        }
    };

    {
        auto f1 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_ways, way_remap, out.cell_to_ways); });
        auto f2 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_addrs, addr_remap, out.cell_to_addrs); });
        auto f3 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_interps, interp_remap, out.cell_to_interps); });
        auto f4 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_admin, admin_remap, out.cell_to_admin, true); });
        f1.get(); f2.get(); f3.get(); f4.get();
    }

    // Rebuild compact string pool
    std::unordered_set<uint32_t> used_offsets;
    for (const auto& w : out.ways) used_offsets.insert(w.name_id);
    for (const auto& a : out.addr_points) { used_offsets.insert(a.housenumber_id); used_offsets.insert(a.street_id); }
    for (const auto& iw : out.interp_ways) used_offsets.insert(iw.street_id);
    for (const auto& ap : out.admin_polygons) used_offsets.insert(ap.name_id);

    const auto& old_sp = full.string_pool.data();
    std::unordered_map<uint32_t, uint32_t> string_remap;
    auto& new_sp = out.string_pool.mutable_data();
    new_sp.clear();

    std::vector<uint32_t> sorted_offsets(used_offsets.begin(), used_offsets.end());
    std::sort(sorted_offsets.begin(), sorted_offsets.end());
    for (uint32_t old_off : sorted_offsets) {
        uint32_t new_off = static_cast<uint32_t>(new_sp.size());
        string_remap[old_off] = new_off;
        const char* str = old_sp.data() + old_off;
        size_t len = std::strlen(str);
        new_sp.insert(new_sp.end(), str, str + len + 1);
    }

    for (auto& w : out.ways) w.name_id = string_remap[w.name_id];
    for (auto& a : out.addr_points) { a.housenumber_id = string_remap[a.housenumber_id]; a.street_id = string_remap[a.street_id]; }
    for (auto& iw : out.interp_ways) iw.street_id = string_remap[iw.street_id];
    for (auto& ap : out.admin_polygons) ap.name_id = string_remap[ap.name_id];

    return out;
}

+    log_phase("      filter: data remap", _ft, _fc);


After the cell map remap:


        f1.get(); f2.get(); f3.get(); f4.get();
    }

    // Rebuild compact string pool

        f1.get(); f2.get(); f3.get(); f4.get();
    }
    log_phase("      filter: cell map remap", _ft, _fc);

    // Rebuild compact string pool


        f1.get(); f2.get(); f3.get(); f4.get();
    }
    log_phase("      filter: cell map remap", _ft, _fc);

    // Rebuild compact string pool

#include "continent_filter.h"

#include <algorithm>
#include <cstring>
#include <future>
#include <unordered_set>

#include <s2/s2cell_id.h>
#include <s2/s2latlng.h>

const ContinentBBox kContinents[] = {
    {"africa",            -35.0,  37.5,  -25.0,  55.0},
    {"asia",              -12.0,  82.0,   25.0, 180.0},
    {"europe",             35.0,  72.0,  -25.0,  45.0},
    {"north-america",       7.0,  84.0, -170.0, -50.0},
    {"south-america",     -56.0,  13.0,  -82.0, -34.0},
    {"oceania",           -50.0,   0.0,  110.0, 180.0},
    {"central-america",     7.0,  23.5, -120.0, -57.0},
    {"antarctica",        -90.0, -60.0, -180.0, 180.0},
};

const size_t kContinentCount = sizeof(kContinents) / sizeof(kContinents[0]);

static bool cell_in_bbox(uint64_t cell_id, const ContinentBBox& bbox) {
    S2CellId cell(cell_id);
    S2LatLng center = cell.ToLatLng();
    double lat = center.lat().degrees();
    double lng = center.lng().degrees();
    return lat >= bbox.min_lat && lat <= bbox.max_lat &&
           lng >= bbox.min_lng && lng <= bbox.max_lng;
}

ParsedData filter_by_bbox(const ParsedData& full, const ContinentBBox& bbox) {
    ParsedData out;
    auto _ft = std::chrono::steady_clock::now();
    auto _fc = CpuTicks::now();

    std::cerr << "    data: ways_sorted=" << full.sorted_way_cells.size()
              << " addrs_sorted=" << full.sorted_addr_cells.size()
              << " interps_sorted=" << full.sorted_interp_cells.size()
              << " ways_map=" << full.cell_to_ways.size()
              << " addrs_map=" << full.cell_to_addrs.size()
              << " interps_map=" << full.cell_to_interps.size()
              << " admin_map=" << full.cell_to_admin.size()
              << std::endl;

    // Filter from hash map
    auto filter_cells_map = [&](const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map,
                                bool mask_interior = false) {
        std::cerr << "    DEBUG filter_cells_map: size=" << cell_map.size() << std::endl;
        std::unordered_set<uint32_t> ids;
        for (const auto& [cell_id, cell_ids] : cell_map) {
            if (cell_in_bbox(cell_id, bbox)) {
                for (uint32_t id : cell_ids)
                    ids.insert(mask_interior ? (id & ID_MASK) : id);
            }
        }
        return ids;
    };

    // Filter from sorted pairs — parallel chunked scan.
    // Split the sorted array into N chunks at cell boundaries, each thread
    // builds its own ID set, then merge.
    auto filter_cells_sorted = [&](const std::vector<CellItemPair>& sorted,
                                   bool mask_interior = false) {
        if (sorted.empty()) return std::unordered_set<uint32_t>{};

        unsigned nthreads = std::max(1u, std::thread::hardware_concurrency() / 4);
        size_t chunk = (sorted.size() + nthreads - 1) / nthreads;
        std::cerr << "    DEBUG filter_cells_sorted: size=" << sorted.size()
                  << " nthreads=" << nthreads << " chunk=" << chunk << std::endl;

        // Find chunk boundaries at cell_id transitions
        std::vector<size_t> bounds = {0};
        for (unsigned t = 1; t < nthreads; t++) {
            size_t target = t * chunk;
            if (target >= sorted.size()) break;
            while (target < sorted.size() && sorted[target].cell_id == sorted[target-1].cell_id)
                target++;
            if (target < sorted.size()) bounds.push_back(target);
        }
        bounds.push_back(sorted.size());

        std::vector<std::unordered_set<uint32_t>> thread_ids(bounds.size() - 1);
        std::vector<std::thread> threads;
        for (size_t t = 0; t + 1 < bounds.size(); t++) {
            threads.emplace_back([&, t]() {
                auto& local = thread_ids[t];
                for (size_t i = bounds[t]; i < bounds[t+1]; ) {
                    uint64_t cell_id = sorted[i].cell_id;
                    bool match = cell_in_bbox(cell_id, bbox);
                    while (i < bounds[t+1] && sorted[i].cell_id == cell_id) {
                        if (match) {
                            uint32_t id = sorted[i].item_id;
                            local.insert(mask_interior ? (id & ID_MASK) : id);
                        }
                        i++;
                    }
                }
            });
        }
        std::cerr << "    DEBUG: spawned " << threads.size() << " threads, "
                  << bounds.size() - 1 << " chunks" << std::endl;
        for (auto& t : threads) t.join();

        // Merge thread-local sets
        size_t largest = 0;
        for (size_t t = 0; t < thread_ids.size(); t++)
            if (thread_ids[t].size() > thread_ids[largest].size()) largest = t;
        auto& merged = thread_ids[largest];
        for (size_t t = 0; t < thread_ids.size(); t++) {
            if (t == largest) continue;
            for (auto id : thread_ids[t]) merged.insert(id);
        }
        return std::move(merged);
    };

    // Use sorted pairs where available (faster linear scan), fall back to hash maps
    auto f_ways = std::async(std::launch::async, [&]{
        return !full.sorted_way_cells.empty()
            ? filter_cells_sorted(full.sorted_way_cells)
            : filter_cells_map(full.cell_to_ways);
    });
    auto f_addrs = std::async(std::launch::async, [&]{
        return !full.sorted_addr_cells.empty()
            ? filter_cells_sorted(full.sorted_addr_cells)
            : filter_cells_map(full.cell_to_addrs);
    });
    auto f_interps = std::async(std::launch::async, [&]{
        return !full.sorted_interp_cells.empty()
            ? filter_cells_sorted(full.sorted_interp_cells)
            : filter_cells_map(full.cell_to_interps);
    });
    auto f_admin = std::async(std::launch::async, [&]{
        return filter_cells_map(full.cell_to_admin, true);
    });

    auto used_way_ids = f_ways.get();
    auto used_addr_ids = f_addrs.get();
    auto used_interp_ids = f_interps.get();
    auto used_admin_ids = f_admin.get();
    log_phase("      filter: ID collection", _ft, _fc);

    // Remap all 4 data types in parallel — each builds its own vectors and remap table
    std::unordered_map<uint32_t, uint32_t> way_remap, addr_remap, interp_remap, admin_remap;

    // Ways
    auto f_remap_ways = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_way_ids.begin(), used_way_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<WayHeader> ways;
        std::vector<NodeCoord> nodes;
        ways.reserve(sorted_ids.size());
        nodes.reserve(sorted_ids.size() * 5);
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(ways.size());
            const auto& w = full.ways[old_id];
            WayHeader nw = w;
            nw.node_offset = static_cast<uint32_t>(nodes.size());
            ways.push_back(nw);
            for (uint8_t n = 0; n < w.node_count; n++)
                nodes.push_back(full.street_nodes[w.node_offset + n]);
        }
        return std::make_tuple(std::move(remap), std::move(ways), std::move(nodes));
    });

    // Addrs
    auto f_remap_addrs = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_addr_ids.begin(), used_addr_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<AddrPoint> addrs;
        addrs.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(addrs.size());
            addrs.push_back(full.addr_points[old_id]);
        }
        return std::make_tuple(std::move(remap), std::move(addrs));
    });

    // Interps
    auto f_remap_interps = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_interp_ids.begin(), used_interp_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<InterpWay> iways;
        std::vector<NodeCoord> inodes;
        iways.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(iways.size());
            const auto& iw = full.interp_ways[old_id];
            InterpWay niw = iw;
            niw.node_offset = static_cast<uint32_t>(inodes.size());
            iways.push_back(niw);
            for (uint8_t n = 0; n < iw.node_count; n++)
                inodes.push_back(full.interp_nodes[iw.node_offset + n]);
        }
        return std::make_tuple(std::move(remap), std::move(iways), std::move(inodes));
    });

    // Admins
    auto f_remap_admins = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_admin_ids.begin(), used_admin_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<AdminPolygon> polys;
        std::vector<NodeCoord> verts;
        polys.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(polys.size());
            const auto& ap = full.admin_polygons[old_id];
            AdminPolygon nap = ap;
            nap.vertex_offset = static_cast<uint32_t>(verts.size());
            polys.push_back(nap);
            for (uint32_t v = 0; v < ap.vertex_count; v++)
                verts.push_back(full.admin_vertices[ap.vertex_offset + v]);
        }
        return std::make_tuple(std::move(remap), std::move(polys), std::move(verts));
    });

    // Collect results
    {
        auto [wr, ways, nodes] = f_remap_ways.get();
        way_remap = std::move(wr);
        out.ways = std::move(ways);
        out.street_nodes = std::move(nodes);
    }
    {
        auto [ar, addrs] = f_remap_addrs.get();
        addr_remap = std::move(ar);
        out.addr_points = std::move(addrs);
    }
    {
        auto [ir, iways, inodes] = f_remap_interps.get();
        interp_remap = std::move(ir);
        out.interp_ways = std::move(iways);
        out.interp_nodes = std::move(inodes);
    }
    {
        auto [ar, polys, verts] = f_remap_admins.get();
        admin_remap = std::move(ar);
        out.admin_polygons = std::move(polys);
        out.admin_vertices = std::move(verts);
    }
    log_phase("      filter: data remap", _ft, _fc);

    // Remap cell maps in parallel (each builds an independent output map)
    auto remap_cells = [&](const std::unordered_map<uint64_t, std::vector<uint32_t>>& src,
                           const std::unordered_map<uint32_t, uint32_t>& remap,
                           std::unordered_map<uint64_t, std::vector<uint32_t>>& dst,
                           bool handle_flags = false) {
        for (const auto& [cell_id, ids] : src) {
            if (!cell_in_bbox(cell_id, bbox)) continue;
            std::vector<uint32_t> new_ids;
            for (uint32_t id : ids) {
                uint32_t raw_id = handle_flags ? (id & ID_MASK) : id;
                uint32_t flags = handle_flags ? (id & INTERIOR_FLAG) : 0;
                auto it = remap.find(raw_id);
                if (it != remap.end()) new_ids.push_back(it->second | flags);
            }
            if (!new_ids.empty()) dst[cell_id] = std::move(new_ids);
        }
    };

    {
        auto f1 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_ways, way_remap, out.cell_to_ways); });
        auto f2 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_addrs, addr_remap, out.cell_to_addrs); });
        auto f3 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_interps, interp_remap, out.cell_to_interps); });
        auto f4 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_admin, admin_remap, out.cell_to_admin, true); });
        f1.get(); f2.get(); f3.get(); f4.get();
    }

    // Rebuild compact string pool
    std::unordered_set<uint32_t> used_offsets;
    for (const auto& w : out.ways) used_offsets.insert(w.name_id);
    for (const auto& a : out.addr_points) { used_offsets.insert(a.housenumber_id); used_offsets.insert(a.street_id); }
    for (const auto& iw : out.interp_ways) used_offsets.insert(iw.street_id);
    for (const auto& ap : out.admin_polygons) used_offsets.insert(ap.name_id);

    const auto& old_sp = full.string_pool.data();
    std::unordered_map<uint32_t, uint32_t> string_remap;
    auto& new_sp = out.string_pool.mutable_data();
    new_sp.clear();

    std::vector<uint32_t> sorted_offsets(used_offsets.begin(), used_offsets.end());
    std::sort(sorted_offsets.begin(), sorted_offsets.end());
    for (uint32_t old_off : sorted_offsets) {
        uint32_t new_off = static_cast<uint32_t>(new_sp.size());
        string_remap[old_off] = new_off;
        const char* str = old_sp.data() + old_off;
        size_t len = std::strlen(str);
        new_sp.insert(new_sp.end(), str, str + len + 1);
    }

    for (auto& w : out.ways) w.name_id = string_remap[w.name_id];
    for (auto& a : out.addr_points) { a.housenumber_id = string_remap[a.housenumber_id]; a.street_id = string_remap[a.street_id]; }
    for (auto& iw : out.interp_ways) iw.street_id = string_remap[iw.street_id];
    for (auto& ap : out.admin_polygons) ap.name_id = string_remap[ap.name_id];

    return out;
}

+    log_phase("      filter: cell map remap", _ft, _fc);


After the string pool rebuild:


    for (auto& ap : out.admin_polygons) ap.name_id = string_remap[ap.name_id];

    return out;

    for (auto& ap : out.admin_polygons) ap.name_id = string_remap[ap.name_id];
    log_phase("      filter: string pool rebuild", _ft, _fc);

    return out;


    for (auto& ap : out.admin_polygons) ap.name_id = string_remap[ap.name_id];
    log_phase("      filter: string pool rebuild", _ft, _fc);

    return out;

#include "continent_filter.h"

#include <algorithm>
#include <cstring>
#include <future>
#include <unordered_set>

#include <s2/s2cell_id.h>
#include <s2/s2latlng.h>

const ContinentBBox kContinents[] = {
    {"africa",            -35.0,  37.5,  -25.0,  55.0},
    {"asia",              -12.0,  82.0,   25.0, 180.0},
    {"europe",             35.0,  72.0,  -25.0,  45.0},
    {"north-america",       7.0,  84.0, -170.0, -50.0},
    {"south-america",     -56.0,  13.0,  -82.0, -34.0},
    {"oceania",           -50.0,   0.0,  110.0, 180.0},
    {"central-america",     7.0,  23.5, -120.0, -57.0},
    {"antarctica",        -90.0, -60.0, -180.0, 180.0},
};

const size_t kContinentCount = sizeof(kContinents) / sizeof(kContinents[0]);

static bool cell_in_bbox(uint64_t cell_id, const ContinentBBox& bbox) {
    S2CellId cell(cell_id);
    S2LatLng center = cell.ToLatLng();
    double lat = center.lat().degrees();
    double lng = center.lng().degrees();
    return lat >= bbox.min_lat && lat <= bbox.max_lat &&
           lng >= bbox.min_lng && lng <= bbox.max_lng;
}

ParsedData filter_by_bbox(const ParsedData& full, const ContinentBBox& bbox) {
    ParsedData out;
    auto _ft = std::chrono::steady_clock::now();
    auto _fc = CpuTicks::now();

    std::cerr << "    data: ways_sorted=" << full.sorted_way_cells.size()
              << " addrs_sorted=" << full.sorted_addr_cells.size()
              << " interps_sorted=" << full.sorted_interp_cells.size()
              << " ways_map=" << full.cell_to_ways.size()
              << " addrs_map=" << full.cell_to_addrs.size()
              << " interps_map=" << full.cell_to_interps.size()
              << " admin_map=" << full.cell_to_admin.size()
              << std::endl;

    // Filter from hash map
    auto filter_cells_map = [&](const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map,
                                bool mask_interior = false) {
        std::cerr << "    DEBUG filter_cells_map: size=" << cell_map.size() << std::endl;
        std::unordered_set<uint32_t> ids;
        for (const auto& [cell_id, cell_ids] : cell_map) {
            if (cell_in_bbox(cell_id, bbox)) {
                for (uint32_t id : cell_ids)
                    ids.insert(mask_interior ? (id & ID_MASK) : id);
            }
        }
        return ids;
    };

    // Filter from sorted pairs — parallel chunked scan.
    // Split the sorted array into N chunks at cell boundaries, each thread
    // builds its own ID set, then merge.
    auto filter_cells_sorted = [&](const std::vector<CellItemPair>& sorted,
                                   bool mask_interior = false) {
        if (sorted.empty()) return std::unordered_set<uint32_t>{};

        unsigned nthreads = std::max(1u, std::thread::hardware_concurrency() / 4);
        size_t chunk = (sorted.size() + nthreads - 1) / nthreads;
        std::cerr << "    DEBUG filter_cells_sorted: size=" << sorted.size()
                  << " nthreads=" << nthreads << " chunk=" << chunk << std::endl;

        // Find chunk boundaries at cell_id transitions
        std::vector<size_t> bounds = {0};
        for (unsigned t = 1; t < nthreads; t++) {
            size_t target = t * chunk;
            if (target >= sorted.size()) break;
            while (target < sorted.size() && sorted[target].cell_id == sorted[target-1].cell_id)
                target++;
            if (target < sorted.size()) bounds.push_back(target);
        }
        bounds.push_back(sorted.size());

        std::vector<std::unordered_set<uint32_t>> thread_ids(bounds.size() - 1);
        std::vector<std::thread> threads;
        for (size_t t = 0; t + 1 < bounds.size(); t++) {
            threads.emplace_back([&, t]() {
                auto& local = thread_ids[t];
                for (size_t i = bounds[t]; i < bounds[t+1]; ) {
                    uint64_t cell_id = sorted[i].cell_id;
                    bool match = cell_in_bbox(cell_id, bbox);
                    while (i < bounds[t+1] && sorted[i].cell_id == cell_id) {
                        if (match) {
                            uint32_t id = sorted[i].item_id;
                            local.insert(mask_interior ? (id & ID_MASK) : id);
                        }
                        i++;
                    }
                }
            });
        }
        std::cerr << "    DEBUG: spawned " << threads.size() << " threads, "
                  << bounds.size() - 1 << " chunks" << std::endl;
        for (auto& t : threads) t.join();

        // Merge thread-local sets
        size_t largest = 0;
        for (size_t t = 0; t < thread_ids.size(); t++)
            if (thread_ids[t].size() > thread_ids[largest].size()) largest = t;
        auto& merged = thread_ids[largest];
        for (size_t t = 0; t < thread_ids.size(); t++) {
            if (t == largest) continue;
            for (auto id : thread_ids[t]) merged.insert(id);
        }
        return std::move(merged);
    };

    // Use sorted pairs where available (faster linear scan), fall back to hash maps
    auto f_ways = std::async(std::launch::async, [&]{
        return !full.sorted_way_cells.empty()
            ? filter_cells_sorted(full.sorted_way_cells)
            : filter_cells_map(full.cell_to_ways);
    });
    auto f_addrs = std::async(std::launch::async, [&]{
        return !full.sorted_addr_cells.empty()
            ? filter_cells_sorted(full.sorted_addr_cells)
            : filter_cells_map(full.cell_to_addrs);
    });
    auto f_interps = std::async(std::launch::async, [&]{
        return !full.sorted_interp_cells.empty()
            ? filter_cells_sorted(full.sorted_interp_cells)
            : filter_cells_map(full.cell_to_interps);
    });
    auto f_admin = std::async(std::launch::async, [&]{
        return filter_cells_map(full.cell_to_admin, true);
    });

    auto used_way_ids = f_ways.get();
    auto used_addr_ids = f_addrs.get();
    auto used_interp_ids = f_interps.get();
    auto used_admin_ids = f_admin.get();
    log_phase("      filter: ID collection", _ft, _fc);

    // Remap all 4 data types in parallel — each builds its own vectors and remap table
    std::unordered_map<uint32_t, uint32_t> way_remap, addr_remap, interp_remap, admin_remap;

    // Ways
    auto f_remap_ways = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_way_ids.begin(), used_way_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<WayHeader> ways;
        std::vector<NodeCoord> nodes;
        ways.reserve(sorted_ids.size());
        nodes.reserve(sorted_ids.size() * 5);
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(ways.size());
            const auto& w = full.ways[old_id];
            WayHeader nw = w;
            nw.node_offset = static_cast<uint32_t>(nodes.size());
            ways.push_back(nw);
            for (uint8_t n = 0; n < w.node_count; n++)
                nodes.push_back(full.street_nodes[w.node_offset + n]);
        }
        return std::make_tuple(std::move(remap), std::move(ways), std::move(nodes));
    });

    // Addrs
    auto f_remap_addrs = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_addr_ids.begin(), used_addr_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<AddrPoint> addrs;
        addrs.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(addrs.size());
            addrs.push_back(full.addr_points[old_id]);
        }
        return std::make_tuple(std::move(remap), std::move(addrs));
    });

    // Interps
    auto f_remap_interps = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_interp_ids.begin(), used_interp_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<InterpWay> iways;
        std::vector<NodeCoord> inodes;
        iways.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(iways.size());
            const auto& iw = full.interp_ways[old_id];
            InterpWay niw = iw;
            niw.node_offset = static_cast<uint32_t>(inodes.size());
            iways.push_back(niw);
            for (uint8_t n = 0; n < iw.node_count; n++)
                inodes.push_back(full.interp_nodes[iw.node_offset + n]);
        }
        return std::make_tuple(std::move(remap), std::move(iways), std::move(inodes));
    });

    // Admins
    auto f_remap_admins = std::async(std::launch::async, [&]() {
        std::vector<uint32_t> sorted_ids(used_admin_ids.begin(), used_admin_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        std::vector<AdminPolygon> polys;
        std::vector<NodeCoord> verts;
        polys.reserve(sorted_ids.size());
        std::unordered_map<uint32_t, uint32_t> remap;
        remap.reserve(sorted_ids.size());
        for (uint32_t old_id : sorted_ids) {
            remap[old_id] = static_cast<uint32_t>(polys.size());
            const auto& ap = full.admin_polygons[old_id];
            AdminPolygon nap = ap;
            nap.vertex_offset = static_cast<uint32_t>(verts.size());
            polys.push_back(nap);
            for (uint32_t v = 0; v < ap.vertex_count; v++)
                verts.push_back(full.admin_vertices[ap.vertex_offset + v]);
        }
        return std::make_tuple(std::move(remap), std::move(polys), std::move(verts));
    });

    // Collect results
    {
        auto [wr, ways, nodes] = f_remap_ways.get();
        way_remap = std::move(wr);
        out.ways = std::move(ways);
        out.street_nodes = std::move(nodes);
    }
    {
        auto [ar, addrs] = f_remap_addrs.get();
        addr_remap = std::move(ar);
        out.addr_points = std::move(addrs);
    }
    {
        auto [ir, iways, inodes] = f_remap_interps.get();
        interp_remap = std::move(ir);
        out.interp_ways = std::move(iways);
        out.interp_nodes = std::move(inodes);
    }
    {
        auto [ar, polys, verts] = f_remap_admins.get();
        admin_remap = std::move(ar);
        out.admin_polygons = std::move(polys);
        out.admin_vertices = std::move(verts);
    }
    log_phase("      filter: data remap", _ft, _fc);

    // Remap cell maps in parallel (each builds an independent output map)
    auto remap_cells = [&](const std::unordered_map<uint64_t, std::vector<uint32_t>>& src,
                           const std::unordered_map<uint32_t, uint32_t>& remap,
                           std::unordered_map<uint64_t, std::vector<uint32_t>>& dst,
                           bool handle_flags = false) {
        for (const auto& [cell_id, ids] : src) {
            if (!cell_in_bbox(cell_id, bbox)) continue;
            std::vector<uint32_t> new_ids;
            for (uint32_t id : ids) {
                uint32_t raw_id = handle_flags ? (id & ID_MASK) : id;
                uint32_t flags = handle_flags ? (id & INTERIOR_FLAG) : 0;
                auto it = remap.find(raw_id);
                if (it != remap.end()) new_ids.push_back(it->second | flags);
            }
            if (!new_ids.empty()) dst[cell_id] = std::move(new_ids);
        }
    };

    {
        auto f1 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_ways, way_remap, out.cell_to_ways); });
        auto f2 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_addrs, addr_remap, out.cell_to_addrs); });
        auto f3 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_interps, interp_remap, out.cell_to_interps); });
        auto f4 = std::async(std::launch::async, [&]{ remap_cells(full.cell_to_admin, admin_remap, out.cell_to_admin, true); });
        f1.get(); f2.get(); f3.get(); f4.get();
    }
    log_phase("      filter: cell map remap", _ft, _fc);

    // Rebuild compact string pool
    std::unordered_set<uint32_t> used_offsets;
    for (const auto& w : out.ways) used_offsets.insert(w.name_id);
    for (const auto& a : out.addr_points) { used_offsets.insert(a.housenumber_id); used_offsets.insert(a.street_id); }
    for (const auto& iw : out.interp_ways) used_offsets.insert(iw.street_id);
    for (const auto& ap : out.admin_polygons) used_offsets.insert(ap.name_id);

    const auto& old_sp = full.string_pool.data();
    std::unordered_map<uint32_t, uint32_t> string_remap;
    auto& new_sp = out.string_pool.mutable_data();
    new_sp.clear();

    std::vector<uint32_t> sorted_offsets(used_offsets.begin(), used_offsets.end());
    std::sort(sorted_offsets.begin(), sorted_offsets.end());
    for (uint32_t old_off : sorted_offsets) {
        uint32_t new_off = static_cast<uint32_t>(new_sp.size());
        string_remap[old_off] = new_off;
        const char* str = old_sp.data() + old_off;
        size_t len = std::strlen(str);
        new_sp.insert(new_sp.end(), str, str + len + 1);
    }

    for (auto& w : out.ways) w.name_id = string_remap[w.name_id];
    for (auto& a : out.addr_points) { a.housenumber_id = string_remap[a.housenumber_id]; a.street_id = string_remap[a.street_id]; }
    for (auto& iw : out.interp_ways) iw.street_id = string_remap[iw.street_id];
    for (auto& ap : out.admin_polygons) ap.name_id = string_remap[ap.name_id];

    return out;
}

+    log_phase("      filter: string pool rebuild", _ft, _fc);


Also need to include the parsed_data.h header for log_phase and CpuTicks:


#include "continent_filter.h"

#include <algorithm>
#include <cstring>
#include <future>
#include <unordered_set>

#include "continent_filter.h"
#include "parsed_data.h"

#include <algorithm>
#include <cstring>
#include <future>
#include <unordered_set>