   700→            auto it = adj.find(last_key);
   701→            if (it == adj.end()) return false;
   702→
   703→            for (const auto& [wi, is_last] : it->second) {
   704→                if (used[wi]) continue;
   705→                auto& endpoint = is_last ? sways[wi].coords.front() : sways[wi].coords.back();
   706→                int64_t new_key = key_fn(endpoint.first, endpoint.second);
   707→
   708→                used[wi] = true;
   709→                path.push_back({wi, is_last});
   710→
   711→                if (try_close(first_key, new_key, path, depth + 1)) return true;
   712→
   713→                path.pop_back();
   714→                used[wi] = false;
   715→            }
   716→            return false;
   717→        }
   718→    };
   719→
   720→    BacktrackState bt{sub_ways, coord_adj, used, coord_key, 0};
   721→
   722→    // Pass 1: Greedy (fast, O(n) per ring)
   723→    for (size_t start_idx = 0; start_idx < sub_ways.size(); start_idx++) {
   724→        if (used[start_idx]) continue;
   725→
   726→        const auto& sg = sub_ways[start_idx];
   727→        int64_t first_key = coord_key(sg.coords.front().first, sg.coords.front().second);
   728→        int64_t last_key = coord_key(sg.coords.back().first, sg.coords.back().second);
   729→
   730→        if (sg.coords.size() >= 4 && first_key == last_key) {
   731→            used[start_idx] = true;
   732→            rings.push_back(sg.coords);
   733→            continue;
   734→        }
   735→
   736→        used[start_idx] = true;
   737→        std::vector<size_t> attempt_used = {start_idx};
   738→        std::vector<std::pair<double,double>> ring = sg.coords;
   739→        int64_t current_last = last_key;
   740→        bool extended = true;
   741→        while (extended && current_last != first_key) {
   742→            extended = false;
   743→            auto it = coord_adj.find(current_last);
   744→            if (it == coord_adj.end()) break;
   745→            for (const auto& [wi, is_last] : it->second) {
   746→                if (used[wi]) continue;
   747→                const auto& sw = sub_ways[wi];
   748→                bool reversed = is_last;
   749→                auto& endpoint = reversed ? sw.coords.front() : sw.coords.back();
   750→                used[wi] = true;
   751→                attempt_used.push_back(wi);
   752→                if (!reversed) ring.insert(ring.end(), sw.coords.begin() + 1, sw.coords.end());
   753→                else for (auto rit = sw.coords.rbegin() + 1; rit != sw.coords.rend(); ++rit) ring.push_back(*rit);
   754→                current_last = coord_key(endpoint.first, endpoint.second);
   755→                extended = true;
   756→                break;
   757→            }
   758→        }
   759→        if (ring.size() >= 4 && current_last == first_key &&
   760→            !ring_has_self_intersection(ring)) {
   761→            rings.push_back(std::move(ring));
   762→        } else {
   763→            for (auto wi : attempt_used) used[wi] = false;
   764→        }
   765→    }
   766→
   767→    // Pass 2: If greedy left unused sub-ways, retry from scratch with backtracking.
   768→    // Backtracking makes correct branch choices but is slower.
   769→    {
   770→        bool any_unused = false;
   771→        for (size_t i = 0; i < used.size(); i++) {
   772→            if (!used[i]) { any_unused = true; break; }
   773→        }
   774→        if (any_unused && sub_ways.size() <= 30) {
   775→            // Only retry for small relations — large ones stall the build
   776→            std::vector<bool> bt_used(sub_ways.size(), false);
   777→            std::vector<std::vector<std::pair<double,double>>> bt_rings;
   778→
   779→            BacktrackState bt2{sub_ways, coord_adj, bt_used, coord_key, 0};
   780→            int total_bt_calls = 0;
   781→            constexpr int MAX_TOTAL_BT_CALLS = 500000; // total budget for entire relation
   782→
   783→            for (size_t si = 0; si < sub_ways.size(); si++) {
   784→                if (bt_used[si]) continue;
   785→                if (total_bt_calls >= MAX_TOTAL_BT_CALLS) break; // budget exceeded
   786→                const auto& sg = sub_ways[si];
   787→                int64_t fk = coord_key(sg.coords.front().first, sg.coords.front().second);
   788→                int64_t lk = coord_key(sg.coords.back().first, sg.coords.back().second);
   789→                if (sg.coords.size() >= 4 && fk == lk) {
   790→                    bt_used[si] = true;
   791→                    bt_rings.push_back(sg.coords);
   792→                    continue;
   793→                }
   794→                bt_used[si] = true;
   795→                bt2.calls = 0;
   796→                std::vector<std::pair<size_t, bool>> path;
   797→                if (bt2.try_close(fk, lk, path, 0)) {
   798→                    std::vector<std::pair<double,double>> r = sg.coords;
   799→                    for (const auto& [wi, rev] : path) {
   800→                        const auto& c = sub_ways[wi].coords;
   801→                        if (!rev) r.insert(r.end(), c.begin() + 1, c.end());
   802→                        else for (auto it2 = c.rbegin() + 1; it2 != c.rend(); ++it2) r.push_back(*it2);
   803→                    }
   804→                    if (r.size() >= 4 && !ring_has_self_intersection(r)) {
   805→                        bt_rings.push_back(std::move(r));
   806→                        continue;
   807→                    }
   808→                }
   809→                bt_used[si] = false;
   810→                total_bt_calls += bt2.calls;
   811→            }
   812→
   813→            // Keep whichever pass produced more rings
   814→            if (bt_rings.size() > rings.size()) {
   815→                rings = std::move(bt_rings);
   816→            }
   817→        }
   818→    }
   819→
   820→    return rings;
   821→}
   822→
   823→// --- Highway filter ---
   824→
   825→static const std::vector<std::string> kExcludedHighways = {
   826→    "footway", "path", "track", "steps", "cycleway",
   827→    "service", "pedestrian", "bridleway", "construction"
   828→};
   829→
   830→static bool is_included_highway(const char* value) {
   831→    for (const auto& excluded : kExcludedHighways) {
   832→        if (excluded == value) return false;
   833→    }
   834→    return true;
   835→}
   836→
   837→// --- Parse house number (leading digits) ---
   838→
   839→static uint32_t parse_house_number(const char* s) {
   840→    if (!s) return 0;
   841→    uint32_t n = 0;
   842→    while (*s >= '0' && *s <= '9') {
   843→        n = n * 10 + (*s - '0');
   844→        s++;
   845→    }
   846→    return n;
   847→}
   848→
   849→// --- Add an address point ---
   850→
   851→static void add_addr_point(ParsedData& data, double lat, double lng,
   852→                           const char* housenumber, const char* street,
   853→                           uint64_t& addr_count_total) {
   854→    uint32_t addr_id = static_cast<uint32_t>(data.addr_points.size());
   855→    data.addr_points.push_back({
   856→        static_cast<float>(lat),
   857→        static_cast<float>(lng),
   858→        data.string_pool.intern(housenumber),
   859→        data.string_pool.intern(street)
   860→    });
   861→
   862→    S2CellId cell = point_to_cell(lat, lng);
   863→    data.cell_to_addrs[cell.id()].push_back(addr_id);
   864→
   865→    addr_count_total++;
   866→    if (addr_count_total % 1000000 == 0) {
   867→        std::cerr << "Collected " << addr_count_total / 1000000 << "M addresses..." << std::endl;
   868→    }
   869→}
   870→
   871→// --- Add an admin polygon ---
   872→
   873→static void add_admin_polygon(ParsedData& data,
   874→                               const std::vector<std::pair<double,double>>& vertices_in,
   875→                               const char* name, uint8_t admin_level,
   876→                               const char* country_code,
   877→                               AdminCoverPool* admin_pool = nullptr) {
   878→    // Normalize ring rotation: start at the vertex with smallest (lat, lng)
   879→    // This ensures identical output regardless of ring assembly order.
   880→    auto vertices = vertices_in;
   881→    if (vertices.size() >= 4 &&
   882→        std::fabs(vertices.front().first - vertices.back().first) < 1e-7 &&
   883→        std::fabs(vertices.front().second - vertices.back().second) < 1e-7) {
   884→        vertices.pop_back();
   885→        auto min_it = std::min_element(vertices.begin(), vertices.end());
   886→        std::rotate(vertices.begin(), min_it, vertices.end());
   887→        vertices.push_back(vertices.front());
   888→    }
   889→
   890→    // Simplify large polygons
   891→    auto simplified = simplify_polygon(vertices, 500);
   892→    if (simplified.size() < 3) return;
   893→
   894→    uint32_t poly_id = static_cast<uint32_t>(data.admin_polygons.size());
   895→    uint32_t vertex_offset = static_cast<uint32_t>(data.admin_vertices.size());
   896→
   897→    for (const auto& [lat, lng] : simplified) {
   898→        data.admin_vertices.push_back({static_cast<float>(lat), static_cast<float>(lng)});
   899→    }
   900→
   901→    AdminPolygon poly{};
   902→    poly.vertex_offset = vertex_offset;
   903→    poly.vertex_count = static_cast<uint16_t>(std::min(simplified.size(), size_t(65535)));
   904→    poly.name_id = data.string_pool.intern(name);
   905→    poly.admin_level = admin_level;
   906→    poly.area = polygon_area(simplified);
   907→    poly.country_code = (country_code && country_code[0] && country_code[1])
   908→        ? static_cast<uint16_t>((country_code[0] << 8) | country_code[1])
   909→        : 0;
   910→    data.admin_polygons.push_back(poly);
   911→
   912→    // Submit S2 cell coverage to thread pool (runs concurrently with PBF reading)
   913→    if (admin_pool) {
   914→        admin_pool->submit(poly_id, std::move(simplified));
   915→    }
   916→}
   917→
   918→// --- Relation collector for parallel admin assembly (runs during pass 1) ---
   919→
   920→struct RelationCollector : public osmium::handler::Handler {
   921→    std::vector<CollectedRelation>& relations;
   922→
   923→    explicit RelationCollector(std::vector<CollectedRelation>& r) : relations(r) {}
   924→
   925→    void relation(const osmium::Relation& rel) {
   926→        const char* boundary = rel.tags()["boundary"];
   927→        if (!boundary) return;
   928→
   929→        bool is_admin = (std::strcmp(boundary, "administrative") == 0);
   930→        bool is_postal = (std::strcmp(boundary, "postal_code") == 0);
   931→        if (!is_admin && !is_postal) return;
   932→
   933→        uint8_t admin_level = 0;
   934→        if (is_admin) {
   935→            const char* level_str = rel.tags()["admin_level"];
   936→            if (!level_str) return;
   937→            admin_level = static_cast<uint8_t>(std::atoi(level_str));
   938→            if (admin_level < 2 || admin_level > 10) return;
   939→        } else {
   940→            admin_level = 11; // use 11 for postal codes
   941→        }
   942→        if (kMaxAdminLevel > 0 && admin_level > kMaxAdminLevel) return;
   943→
   944→        const char* name = rel.tags()["name"];
   945→        if (!name && is_admin) return;
   946→
   947→        // For postal codes, use postal_code tag as name
   948→        std::string name_str;
   949→        if (is_postal) {
   950→            const char* postal_code = rel.tags()["postal_code"];
   951→            if (!postal_code) postal_code = name;
   952→            if (!postal_code) return;
   953→            name_str = postal_code;
   954→        } else {
   955→            name_str = name;
   956→        }
   957→
   958→        // Extract country code for level 2 boundaries
   959→        std::string country_code;
   960→        if (admin_level == 2) {
   961→            const char* cc = rel.tags()["ISO3166-1:alpha2"];
   962→            if (cc) country_code = cc;
   963→        }
   964→
   965→        // Collect member way IDs and roles
   966→        CollectedRelation cr;
   967→        cr.id = rel.id();
   968→        cr.admin_level = admin_level;
   969→        cr.name = std::move(name_str);
   970→        cr.country_code = std::move(country_code);
   971→        cr.is_postal = is_postal;
   972→
   973→        for (const auto& member : rel.members()) {
   974→            if (member.type() == osmium::item_type::way) {
   975→                cr.members.emplace_back(member.ref(), member.role());
   976→            }
   977→        }
   978→
   979→        if (!cr.members.empty()) {
   980→            relations.push_back(std::move(cr));
   981→        }
   982→    }
   983→};
   984→
   985→// --- Resolve interpolation way endpoint house numbers ---
   986→
   987→static void resolve_interpolation_endpoints(ParsedData& data) {
   988→    struct CoordKey {
   989→        int32_t lat;
   990→        int32_t lng;
   991→        bool operator==(const CoordKey& o) const { return lat == o.lat && lng == o.lng; }
   992→    };
   993→    struct CoordHash {
   994→        size_t operator()(const CoordKey& k) const {
   995→            return std::hash<int64_t>()(((int64_t)k.lat << 32) | (uint32_t)k.lng);
   996→        }
   997→    };
   998→
   999→    std::unordered_map<CoordKey, uint32_t, CoordHash> addr_by_coord;

            auto it = adj.find(last_key);
            if (it == adj.end()) return false;

            for (const auto& [wi, is_last] : it->second) {
                if (used[wi]) continue;
                auto& endpoint = is_last ? sways[wi].coords.front() : sways[wi].coords.back();
                int64_t new_key = key_fn(endpoint.first, endpoint.second);

                used[wi] = true;
                path.push_back({wi, is_last});

                if (try_close(first_key, new_key, path, depth + 1)) return true;

                path.pop_back();
                used[wi] = false;
            }
            return false;
        }
    };

    BacktrackState bt{sub_ways, coord_adj, used, coord_key, 0};

    // Pass 1: Greedy (fast, O(n) per ring)
    for (size_t start_idx = 0; start_idx < sub_ways.size(); start_idx++) {
        if (used[start_idx]) continue;

        const auto& sg = sub_ways[start_idx];
        int64_t first_key = coord_key(sg.coords.front().first, sg.coords.front().second);
        int64_t last_key = coord_key(sg.coords.back().first, sg.coords.back().second);

        if (sg.coords.size() >= 4 && first_key == last_key) {
            used[start_idx] = true;
            rings.push_back(sg.coords);
            continue;
        }

        used[start_idx] = true;
        std::vector<size_t> attempt_used = {start_idx};
        std::vector<std::pair<double,double>> ring = sg.coords;
        int64_t current_last = last_key;
        bool extended = true;
        while (extended && current_last != first_key) {
            extended = false;
            auto it = coord_adj.find(current_last);
            if (it == coord_adj.end()) break;
            for (const auto& [wi, is_last] : it->second) {
                if (used[wi]) continue;
                const auto& sw = sub_ways[wi];
                bool reversed = is_last;
                auto& endpoint = reversed ? sw.coords.front() : sw.coords.back();
                used[wi] = true;
                attempt_used.push_back(wi);
                if (!reversed) ring.insert(ring.end(), sw.coords.begin() + 1, sw.coords.end());
                else for (auto rit = sw.coords.rbegin() + 1; rit != sw.coords.rend(); ++rit) ring.push_back(*rit);
                current_last = coord_key(endpoint.first, endpoint.second);
                extended = true;
                break;
            }
        }
        if (ring.size() >= 4 && current_last == first_key &&
            !ring_has_self_intersection(ring)) {
            rings.push_back(std::move(ring));
        } else {
            for (auto wi : attempt_used) used[wi] = false;
        }
    }

    // Pass 2: If greedy left unused sub-ways, retry from scratch with backtracking.
    // Backtracking makes correct branch choices but is slower.
    {
        bool any_unused = false;
        for (size_t i = 0; i < used.size(); i++) {
            if (!used[i]) { any_unused = true; break; }
        }
        if (any_unused && sub_ways.size() <= 30) {
            // Only retry for small relations — large ones stall the build
            std::vector<bool> bt_used(sub_ways.size(), false);
            std::vector<std::vector<std::pair<double,double>>> bt_rings;

            BacktrackState bt2{sub_ways, coord_adj, bt_used, coord_key, 0};
            int total_bt_calls = 0;
            constexpr int MAX_TOTAL_BT_CALLS = 500000; // total budget for entire relation

            for (size_t si = 0; si < sub_ways.size(); si++) {
                if (bt_used[si]) continue;
                if (total_bt_calls >= MAX_TOTAL_BT_CALLS) break; // budget exceeded
                const auto& sg = sub_ways[si];
                int64_t fk = coord_key(sg.coords.front().first, sg.coords.front().second);
                int64_t lk = coord_key(sg.coords.back().first, sg.coords.back().second);
                if (sg.coords.size() >= 4 && fk == lk) {
                    bt_used[si] = true;
                    bt_rings.push_back(sg.coords);
                    continue;
                }
                bt_used[si] = true;
                bt2.calls = 0;
                std::vector<std::pair<size_t, bool>> path;
                if (bt2.try_close(fk, lk, path, 0)) {
                    std::vector<std::pair<double,double>> r = sg.coords;
                    for (const auto& [wi, rev] : path) {
                        const auto& c = sub_ways[wi].coords;
                        if (!rev) r.insert(r.end(), c.begin() + 1, c.end());
                        else for (auto it2 = c.rbegin() + 1; it2 != c.rend(); ++it2) r.push_back(*it2);
                    }
                    if (r.size() >= 4 && !ring_has_self_intersection(r)) {
                        bt_rings.push_back(std::move(r));
                        continue;
                    }
                }
                bt_used[si] = false;
                total_bt_calls += bt2.calls;
            }

            // Keep whichever pass produced more rings
            if (bt_rings.size() > rings.size()) {
                rings = std::move(bt_rings);
            }
        }
    }

    return rings;
}

// --- Highway filter ---

static const std::vector<std::string> kExcludedHighways = {
    "footway", "path", "track", "steps", "cycleway",
    "service", "pedestrian", "bridleway", "construction"
};

static bool is_included_highway(const char* value) {
    for (const auto& excluded : kExcludedHighways) {
        if (excluded == value) return false;
    }
    return true;
}

// --- Parse house number (leading digits) ---

static uint32_t parse_house_number(const char* s) {
    if (!s) return 0;
    uint32_t n = 0;
    while (*s >= '0' && *s <= '9') {
        n = n * 10 + (*s - '0');
        s++;
    }
    return n;
}

// --- Add an address point ---

static void add_addr_point(ParsedData& data, double lat, double lng,
                           const char* housenumber, const char* street,
                           uint64_t& addr_count_total) {
    uint32_t addr_id = static_cast<uint32_t>(data.addr_points.size());
    data.addr_points.push_back({
        static_cast<float>(lat),
        static_cast<float>(lng),
        data.string_pool.intern(housenumber),
        data.string_pool.intern(street)
    });

    S2CellId cell = point_to_cell(lat, lng);
    data.cell_to_addrs[cell.id()].push_back(addr_id);

    addr_count_total++;
    if (addr_count_total % 1000000 == 0) {
        std::cerr << "Collected " << addr_count_total / 1000000 << "M addresses..." << std::endl;
    }
}

// --- Add an admin polygon ---

static void add_admin_polygon(ParsedData& data,
                               const std::vector<std::pair<double,double>>& vertices_in,
                               const char* name, uint8_t admin_level,
                               const char* country_code,
                               AdminCoverPool* admin_pool = nullptr) {
    // Normalize ring rotation: start at the vertex with smallest (lat, lng)
    // This ensures identical output regardless of ring assembly order.
    auto vertices = vertices_in;
    if (vertices.size() >= 4 &&
        std::fabs(vertices.front().first - vertices.back().first) < 1e-7 &&
        std::fabs(vertices.front().second - vertices.back().second) < 1e-7) {
        vertices.pop_back();
        auto min_it = std::min_element(vertices.begin(), vertices.end());
        std::rotate(vertices.begin(), min_it, vertices.end());
        vertices.push_back(vertices.front());
    }

    // Simplify large polygons
    auto simplified = simplify_polygon(vertices, 500);
    if (simplified.size() < 3) return;

    uint32_t poly_id = static_cast<uint32_t>(data.admin_polygons.size());
    uint32_t vertex_offset = static_cast<uint32_t>(data.admin_vertices.size());

    for (const auto& [lat, lng] : simplified) {
        data.admin_vertices.push_back({static_cast<float>(lat), static_cast<float>(lng)});
    }

    AdminPolygon poly{};
    poly.vertex_offset = vertex_offset;
    poly.vertex_count = static_cast<uint16_t>(std::min(simplified.size(), size_t(65535)));
    poly.name_id = data.string_pool.intern(name);
    poly.admin_level = admin_level;
    poly.area = polygon_area(simplified);
    poly.country_code = (country_code && country_code[0] && country_code[1])
        ? static_cast<uint16_t>((country_code[0] << 8) | country_code[1])
        : 0;
    data.admin_polygons.push_back(poly);

    // Submit S2 cell coverage to thread pool (runs concurrently with PBF reading)
    if (admin_pool) {
        admin_pool->submit(poly_id, std::move(simplified));
    }
}

// --- Relation collector for parallel admin assembly (runs during pass 1) ---

struct RelationCollector : public osmium::handler::Handler {
    std::vector<CollectedRelation>& relations;

    explicit RelationCollector(std::vector<CollectedRelation>& r) : relations(r) {}

    void relation(const osmium::Relation& rel) {
        const char* boundary = rel.tags()["boundary"];
        if (!boundary) return;

        bool is_admin = (std::strcmp(boundary, "administrative") == 0);
        bool is_postal = (std::strcmp(boundary, "postal_code") == 0);
        if (!is_admin && !is_postal) return;

        uint8_t admin_level = 0;
        if (is_admin) {
            const char* level_str = rel.tags()["admin_level"];
            if (!level_str) return;
            admin_level = static_cast<uint8_t>(std::atoi(level_str));
            if (admin_level < 2 || admin_level > 10) return;
        } else {
            admin_level = 11; // use 11 for postal codes
        }
        if (kMaxAdminLevel > 0 && admin_level > kMaxAdminLevel) return;

        const char* name = rel.tags()["name"];
        if (!name && is_admin) return;

        // For postal codes, use postal_code tag as name
        std::string name_str;
        if (is_postal) {
            const char* postal_code = rel.tags()["postal_code"];
            if (!postal_code) postal_code = name;
            if (!postal_code) return;
            name_str = postal_code;
        } else {
            name_str = name;
        }

        // Extract country code for level 2 boundaries
        std::string country_code;
        if (admin_level == 2) {
            const char* cc = rel.tags()["ISO3166-1:alpha2"];
            if (cc) country_code = cc;
        }

        // Collect member way IDs and roles
        CollectedRelation cr;
        cr.id = rel.id();
        cr.admin_level = admin_level;
        cr.name = std::move(name_str);
        cr.country_code = std::move(country_code);
        cr.is_postal = is_postal;

        for (const auto& member : rel.members()) {
            if (member.type() == osmium::item_type::way) {
                cr.members.emplace_back(member.ref(), member.role());
            }
        }

        if (!cr.members.empty()) {
            relations.push_back(std::move(cr));
        }
    }
};

// --- Resolve interpolation way endpoint house numbers ---

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

    std::unordered_map<CoordKey, uint32_t, CoordHash> addr_by_coord;


  1000→    for (uint32_t i = 0; i < data.addr_points.size(); i++) {
  1001→        CoordKey key{
  1002→            static_cast<int32_t>(data.addr_points[i].lat * 100000),
  1003→            static_cast<int32_t>(data.addr_points[i].lng * 100000)
  1004→        };
  1005→        addr_by_coord[key] = i;
  1006→    }
  1007→
  1008→    uint32_t resolved = 0;
  1009→    for (auto& iw : data.interp_ways) {
  1010→        if (iw.node_count < 2) continue;
  1011→
  1012→        const auto& start = data.interp_nodes[iw.node_offset];
  1013→        CoordKey start_key{
  1014→            static_cast<int32_t>(start.lat * 100000),
  1015→            static_cast<int32_t>(start.lng * 100000)
  1016→        };
  1017→        auto it_start = addr_by_coord.find(start_key);
  1018→
  1019→        const auto& end = data.interp_nodes[iw.node_offset + iw.node_count - 1];
  1020→        CoordKey end_key{
  1021→            static_cast<int32_t>(end.lat * 100000),
  1022→            static_cast<int32_t>(end.lng * 100000)
  1023→        };
  1024→        auto it_end = addr_by_coord.find(end_key);
  1025→
  1026→        if (it_start != addr_by_coord.end()) {
  1027→            const char* hn = data.string_pool.data().data() + data.addr_points[it_start->second].housenumber_id;
  1028→            iw.start_number = parse_house_number(hn);
  1029→        }
  1030→        if (it_end != addr_by_coord.end()) {
  1031→            const char* hn = data.string_pool.data().data() + data.addr_points[it_end->second].housenumber_id;
  1032→            iw.end_number = parse_house_number(hn);
  1033→        }
  1034→
  1035→        if (iw.start_number > 0 && iw.end_number > 0) resolved++;
  1036→    }
  1037→
  1038→    std::cerr << "Resolved " << resolved << "/" << data.interp_ways.size()
  1039→              << " interpolation ways" << std::endl;
  1040→}
  1041→
  1042→// --- Deduplicate IDs per cell ---
  1043→
  1044→template<typename Map>
  1045→static void deduplicate(Map& cell_map) {
  1046→    for (auto& [cell_id, ids] : cell_map) {
  1047→        std::sort(ids.begin(), ids.end());
  1048→        ids.erase(std::unique(ids.begin(), ids.end()), ids.end());
  1049→    }
  1050→}
  1051→
  1052→// --- Cache serialization ---
  1053→
  1054→enum class SectionType : uint32_t {
  1055→    STRING_POOL = 0,
  1056→    WAYS = 1,
  1057→    STREET_NODES = 2,
  1058→    CELL_TO_WAYS = 3,
  1059→    ADDR_POINTS = 4,
  1060→    CELL_TO_ADDRS = 5,
  1061→    INTERP_WAYS = 6,
  1062→    INTERP_NODES = 7,
  1063→    CELL_TO_INTERPS = 8,
  1064→    ADMIN_POLYGONS = 9,
  1065→    ADMIN_VERTICES = 10,
  1066→    CELL_TO_ADMIN = 11,
  1067→};
  1068→
  1069→static const char CACHE_MAGIC[8] = {'T','G','C','A','C','H','E','\0'};
  1070→static const uint32_t CACHE_VERSION = 1;
  1071→static const uint32_t CACHE_SECTION_COUNT = 12;
  1072→
  1073→// Serialize a vector of structs to a binary blob
  1074→template<typename T>
  1075→static std::vector<char> serialize_vector(const std::vector<T>& vec) {
  1076→    std::vector<char> buf;
  1077→    uint64_t count = vec.size();
  1078→    buf.resize(sizeof(uint64_t) + count * sizeof(T));
  1079→    std::memcpy(buf.data(), &count, sizeof(uint64_t));
  1080→    if (count > 0) {
  1081→        std::memcpy(buf.data() + sizeof(uint64_t), vec.data(), count * sizeof(T));
  1082→    }
  1083→    return buf;
  1084→}
  1085→
  1086→// Deserialize a vector of structs from a binary blob
  1087→template<typename T>
  1088→static bool deserialize_vector(const char* data, uint64_t length, std::vector<T>& vec) {
  1089→    if (length < sizeof(uint64_t)) return false;
  1090→    uint64_t count;
  1091→    std::memcpy(&count, data, sizeof(uint64_t));
  1092→    if (length < sizeof(uint64_t) + count * sizeof(T)) return false;
  1093→    vec.resize(count);
  1094→    if (count > 0) {
  1095→        std::memcpy(vec.data(), data + sizeof(uint64_t), count * sizeof(T));
  1096→    }
  1097→    return true;
  1098→}
  1099→
  1100→// Serialize a cell map (uint64_t -> vector<uint32_t>)
  1101→static std::vector<char> serialize_cell_map(const std::unordered_map<uint64_t, std::vector<uint32_t>>& map) {
  1102→    // Calculate total size
  1103→    size_t total = sizeof(uint64_t); // entry count
  1104→    for (const auto& [cell_id, ids] : map) {
  1105→        total += sizeof(uint64_t) + sizeof(uint32_t) + ids.size() * sizeof(uint32_t);
  1106→    }
  1107→    std::vector<char> buf(total);
  1108→    char* ptr = buf.data();
  1109→
  1110→    uint64_t entry_count = map.size();
  1111→    std::memcpy(ptr, &entry_count, sizeof(uint64_t));
  1112→    ptr += sizeof(uint64_t);
  1113→
  1114→    for (const auto& [cell_id, ids] : map) {
  1115→        std::memcpy(ptr, &cell_id, sizeof(uint64_t));
  1116→        ptr += sizeof(uint64_t);
  1117→        uint32_t id_count = static_cast<uint32_t>(ids.size());
  1118→        std::memcpy(ptr, &id_count, sizeof(uint32_t));
  1119→        ptr += sizeof(uint32_t);
  1120→        if (id_count > 0) {
  1121→            std::memcpy(ptr, ids.data(), id_count * sizeof(uint32_t));
  1122→            ptr += id_count * sizeof(uint32_t);
  1123→        }
  1124→    }
  1125→    return buf;
  1126→}
  1127→
  1128→// Deserialize a cell map
  1129→static bool deserialize_cell_map(const char* data, uint64_t length,
  1130→                                  std::unordered_map<uint64_t, std::vector<uint32_t>>& map) {
  1131→    if (length < sizeof(uint64_t)) return false;
  1132→    const char* ptr = data;
  1133→    const char* end = data + length;
  1134→
  1135→    uint64_t entry_count;
  1136→    std::memcpy(&entry_count, ptr, sizeof(uint64_t));
  1137→    ptr += sizeof(uint64_t);
  1138→
  1139→    map.reserve(entry_count);
  1140→    for (uint64_t i = 0; i < entry_count; i++) {
  1141→        if (ptr + sizeof(uint64_t) + sizeof(uint32_t) > end) return false;
  1142→        uint64_t cell_id;
  1143→        std::memcpy(&cell_id, ptr, sizeof(uint64_t));
  1144→        ptr += sizeof(uint64_t);
  1145→        uint32_t id_count;
  1146→        std::memcpy(&id_count, ptr, sizeof(uint32_t));
  1147→        ptr += sizeof(uint32_t);
  1148→        if (ptr + id_count * sizeof(uint32_t) > end) return false;
  1149→        std::vector<uint32_t> ids(id_count);
  1150→        if (id_count > 0) {
  1151→            std::memcpy(ids.data(), ptr, id_count * sizeof(uint32_t));
  1152→            ptr += id_count * sizeof(uint32_t);
  1153→        }
  1154→        map[cell_id] = std::move(ids);
  1155→    }
  1156→    return true;
  1157→}
  1158→
  1159→static void serialize_cache(const ParsedData& data, const std::string& path) {
  1160→    std::cerr << "Saving cache to " << path << "..." << std::endl;
  1161→    std::ofstream f(path, std::ios::binary);
  1162→    if (!f.is_open()) {
  1163→        std::cerr << "Error: could not open cache file for writing: " << path << std::endl;
  1164→        return;
  1165→    }
  1166→
  1167→    f.write(CACHE_MAGIC, 8);
  1168→    f.write(reinterpret_cast<const char*>(&CACHE_VERSION), sizeof(uint32_t));
  1169→    f.write(reinterpret_cast<const char*>(&CACHE_SECTION_COUNT), sizeof(uint32_t));
  1170→
  1171→    // Helper to write a section
  1172→    auto write_section = [&](SectionType type, const std::vector<char>& blob) {
  1173→        uint32_t t = static_cast<uint32_t>(type);
  1174→        uint64_t len = blob.size();
  1175→        f.write(reinterpret_cast<const char*>(&t), sizeof(uint32_t));
  1176→        f.write(reinterpret_cast<const char*>(&len), sizeof(uint64_t));
  1177→        f.write(blob.data(), len);
  1178→    };
  1179→
  1180→    // STRING_POOL: raw char vector
  1181→    {
  1182→        const auto& sp = data.string_pool.data();
  1183→        std::vector<char> blob(sizeof(uint64_t) + sp.size());
  1184→        uint64_t sz = sp.size();
  1185→        std::memcpy(blob.data(), &sz, sizeof(uint64_t));
  1186→        if (sz > 0) std::memcpy(blob.data() + sizeof(uint64_t), sp.data(), sz);
  1187→        write_section(SectionType::STRING_POOL, blob);
  1188→    }
  1189→
  1190→    write_section(SectionType::WAYS, serialize_vector(data.ways));
  1191→    write_section(SectionType::STREET_NODES, serialize_vector(data.street_nodes));
  1192→    write_section(SectionType::CELL_TO_WAYS, serialize_cell_map(data.cell_to_ways));
  1193→    write_section(SectionType::ADDR_POINTS, serialize_vector(data.addr_points));
  1194→    write_section(SectionType::CELL_TO_ADDRS, serialize_cell_map(data.cell_to_addrs));
  1195→    write_section(SectionType::INTERP_WAYS, serialize_vector(data.interp_ways));
  1196→    write_section(SectionType::INTERP_NODES, serialize_vector(data.interp_nodes));
  1197→    write_section(SectionType::CELL_TO_INTERPS, serialize_cell_map(data.cell_to_interps));
  1198→    write_section(SectionType::ADMIN_POLYGONS, serialize_vector(data.admin_polygons));
  1199→    write_section(SectionType::ADMIN_VERTICES, serialize_vector(data.admin_vertices));
  1200→    write_section(SectionType::CELL_TO_ADMIN, serialize_cell_map(data.cell_to_admin));
  1201→
  1202→    std::cerr << "Cache saved." << std::endl;
  1203→}
  1204→
  1205→static bool deserialize_cache(ParsedData& data, const std::string& path) {
  1206→    std::cerr << "Loading cache from " << path << "..." << std::endl;
  1207→    std::ifstream f(path, std::ios::binary);
  1208→    if (!f.is_open()) {
  1209→        std::cerr << "Error: could not open cache file: " << path << std::endl;
  1210→        return false;
  1211→    }
  1212→
  1213→    char magic[8];
  1214→    f.read(magic, 8);
  1215→    if (std::memcmp(magic, CACHE_MAGIC, 8) != 0) {
  1216→        std::cerr << "Error: invalid cache magic" << std::endl;
  1217→        return false;
  1218→    }
  1219→
  1220→    uint32_t version;
  1221→    f.read(reinterpret_cast<char*>(&version), sizeof(uint32_t));
  1222→    if (version != CACHE_VERSION) {
  1223→        std::cerr << "Error: unsupported cache version " << version << std::endl;
  1224→        return false;
  1225→    }
  1226→
  1227→    uint32_t section_count;
  1228→    f.read(reinterpret_cast<char*>(&section_count), sizeof(uint32_t));
  1229→
  1230→    for (uint32_t s = 0; s < section_count; s++) {
  1231→        uint32_t type;
  1232→        uint64_t length;
  1233→        f.read(reinterpret_cast<char*>(&type), sizeof(uint32_t));
  1234→        f.read(reinterpret_cast<char*>(&length), sizeof(uint64_t));
  1235→
  1236→        std::vector<char> blob(length);
  1237→        f.read(blob.data(), length);
  1238→
  1239→        switch (static_cast<SectionType>(type)) {
  1240→        case SectionType::STRING_POOL: {
  1241→            if (length < sizeof(uint64_t)) return false;
  1242→            uint64_t sz;
  1243→            std::memcpy(&sz, blob.data(), sizeof(uint64_t));
  1244→            auto& sp = data.string_pool.mutable_data();
  1245→            sp.resize(sz);
  1246→            if (sz > 0) std::memcpy(sp.data(), blob.data() + sizeof(uint64_t), sz);
  1247→            break;
  1248→        }
  1249→        case SectionType::WAYS:
  1250→            if (!deserialize_vector(blob.data(), length, data.ways)) return false;
  1251→            break;
  1252→        case SectionType::STREET_NODES:
  1253→            if (!deserialize_vector(blob.data(), length, data.street_nodes)) return false;
  1254→            break;
  1255→        case SectionType::CELL_TO_WAYS:
  1256→            if (!deserialize_cell_map(blob.data(), length, data.cell_to_ways)) return false;
  1257→            break;
  1258→        case SectionType::ADDR_POINTS:
  1259→            if (!deserialize_vector(blob.data(), length, data.addr_points)) return false;
  1260→            break;
  1261→        case SectionType::CELL_TO_ADDRS:
  1262→            if (!deserialize_cell_map(blob.data(), length, data.cell_to_addrs)) return false;
  1263→            break;
  1264→        case SectionType::INTERP_WAYS:
  1265→            if (!deserialize_vector(blob.data(), length, data.interp_ways)) return false;
  1266→            break;
  1267→        case SectionType::INTERP_NODES:
  1268→            if (!deserialize_vector(blob.data(), length, data.interp_nodes)) return false;
  1269→            break;
  1270→        case SectionType::CELL_TO_INTERPS:
  1271→            if (!deserialize_cell_map(blob.data(), length, data.cell_to_interps)) return false;
  1272→            break;
  1273→        case SectionType::ADMIN_POLYGONS:
  1274→            if (!deserialize_vector(blob.data(), length, data.admin_polygons)) return false;
  1275→            break;
  1276→        case SectionType::ADMIN_VERTICES:
  1277→            if (!deserialize_vector(blob.data(), length, data.admin_vertices)) return false;
  1278→            break;
  1279→        case SectionType::CELL_TO_ADMIN:
  1280→            if (!deserialize_cell_map(blob.data(), length, data.cell_to_admin)) return false;
  1281→            break;
  1282→        default:
  1283→            std::cerr << "Warning: unknown cache section type " << type << ", skipping" << std::endl;
  1284→            break;
  1285→        }
  1286→    }
  1287→
  1288→    std::cerr << "Cache loaded: " << data.ways.size() << " ways, "
  1289→              << data.addr_points.size() << " addrs, "
  1290→              << data.interp_ways.size() << " interps, "
  1291→              << data.admin_polygons.size() << " admin polygons" << std::endl;
  1292→    return true;
  1293→}
  1294→
  1295→// --- Continent bounding box filtering ---
  1296→
  1297→struct ContinentBBox {
  1298→    const char* name;
  1299→    double min_lat, max_lat, min_lng, max_lng;

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

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

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

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

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

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

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

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

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

// --- Cache serialization ---

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

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

// Serialize a vector of structs to a binary blob
template<typename T>
static std::vector<char> serialize_vector(const std::vector<T>& vec) {
    std::vector<char> buf;
    uint64_t count = vec.size();
    buf.resize(sizeof(uint64_t) + count * sizeof(T));
    std::memcpy(buf.data(), &count, sizeof(uint64_t));
    if (count > 0) {
        std::memcpy(buf.data() + sizeof(uint64_t), vec.data(), count * sizeof(T));
    }
    return buf;
}

// Deserialize a vector of structs from a binary blob
template<typename T>
static bool deserialize_vector(const char* data, uint64_t length, std::vector<T>& vec) {
    if (length < sizeof(uint64_t)) return false;
    uint64_t count;
    std::memcpy(&count, data, sizeof(uint64_t));
    if (length < sizeof(uint64_t) + count * sizeof(T)) return false;
    vec.resize(count);
    if (count > 0) {
        std::memcpy(vec.data(), data + sizeof(uint64_t), count * sizeof(T));
    }
    return true;
}

// Serialize a cell map (uint64_t -> vector<uint32_t>)
static std::vector<char> serialize_cell_map(const std::unordered_map<uint64_t, std::vector<uint32_t>>& map) {
    // Calculate total size
    size_t total = sizeof(uint64_t); // entry count
    for (const auto& [cell_id, ids] : map) {
        total += sizeof(uint64_t) + sizeof(uint32_t) + ids.size() * sizeof(uint32_t);
    }
    std::vector<char> buf(total);
    char* ptr = buf.data();

    uint64_t entry_count = map.size();
    std::memcpy(ptr, &entry_count, sizeof(uint64_t));
    ptr += sizeof(uint64_t);

    for (const auto& [cell_id, ids] : map) {
        std::memcpy(ptr, &cell_id, sizeof(uint64_t));
        ptr += sizeof(uint64_t);
        uint32_t id_count = static_cast<uint32_t>(ids.size());
        std::memcpy(ptr, &id_count, sizeof(uint32_t));
        ptr += sizeof(uint32_t);
        if (id_count > 0) {
            std::memcpy(ptr, ids.data(), id_count * sizeof(uint32_t));
            ptr += id_count * sizeof(uint32_t);
        }
    }
    return buf;
}

// Deserialize a cell map
static bool deserialize_cell_map(const char* data, uint64_t length,
                                  std::unordered_map<uint64_t, std::vector<uint32_t>>& map) {
    if (length < sizeof(uint64_t)) return false;
    const char* ptr = data;
    const char* end = data + length;

    uint64_t entry_count;
    std::memcpy(&entry_count, ptr, sizeof(uint64_t));
    ptr += sizeof(uint64_t);

    map.reserve(entry_count);
    for (uint64_t i = 0; i < entry_count; i++) {
        if (ptr + sizeof(uint64_t) + sizeof(uint32_t) > end) return false;
        uint64_t cell_id;
        std::memcpy(&cell_id, ptr, sizeof(uint64_t));
        ptr += sizeof(uint64_t);
        uint32_t id_count;
        std::memcpy(&id_count, ptr, sizeof(uint32_t));
        ptr += sizeof(uint32_t);
        if (ptr + id_count * sizeof(uint32_t) > end) return false;
        std::vector<uint32_t> ids(id_count);
        if (id_count > 0) {
            std::memcpy(ids.data(), ptr, id_count * sizeof(uint32_t));
            ptr += id_count * sizeof(uint32_t);
        }
        map[cell_id] = std::move(ids);
    }
    return true;
}

static void serialize_cache(const ParsedData& data, const std::string& path) {
    std::cerr << "Saving cache to " << path << "..." << std::endl;
    std::ofstream f(path, std::ios::binary);
    if (!f.is_open()) {
        std::cerr << "Error: could not open cache file for writing: " << path << std::endl;
        return;
    }

    f.write(CACHE_MAGIC, 8);
    f.write(reinterpret_cast<const char*>(&CACHE_VERSION), sizeof(uint32_t));
    f.write(reinterpret_cast<const char*>(&CACHE_SECTION_COUNT), sizeof(uint32_t));

    // Helper to write a section
    auto write_section = [&](SectionType type, const std::vector<char>& blob) {
        uint32_t t = static_cast<uint32_t>(type);
        uint64_t len = blob.size();
        f.write(reinterpret_cast<const char*>(&t), sizeof(uint32_t));
        f.write(reinterpret_cast<const char*>(&len), sizeof(uint64_t));
        f.write(blob.data(), len);
    };

    // STRING_POOL: raw char vector
    {
        const auto& sp = data.string_pool.data();
        std::vector<char> blob(sizeof(uint64_t) + sp.size());
        uint64_t sz = sp.size();
        std::memcpy(blob.data(), &sz, sizeof(uint64_t));
        if (sz > 0) std::memcpy(blob.data() + sizeof(uint64_t), sp.data(), sz);
        write_section(SectionType::STRING_POOL, blob);
    }

    write_section(SectionType::WAYS, serialize_vector(data.ways));
    write_section(SectionType::STREET_NODES, serialize_vector(data.street_nodes));
    write_section(SectionType::CELL_TO_WAYS, serialize_cell_map(data.cell_to_ways));
    write_section(SectionType::ADDR_POINTS, serialize_vector(data.addr_points));
    write_section(SectionType::CELL_TO_ADDRS, serialize_cell_map(data.cell_to_addrs));
    write_section(SectionType::INTERP_WAYS, serialize_vector(data.interp_ways));
    write_section(SectionType::INTERP_NODES, serialize_vector(data.interp_nodes));
    write_section(SectionType::CELL_TO_INTERPS, serialize_cell_map(data.cell_to_interps));
    write_section(SectionType::ADMIN_POLYGONS, serialize_vector(data.admin_polygons));
    write_section(SectionType::ADMIN_VERTICES, serialize_vector(data.admin_vertices));
    write_section(SectionType::CELL_TO_ADMIN, serialize_cell_map(data.cell_to_admin));

    std::cerr << "Cache saved." << std::endl;
}

static bool deserialize_cache(ParsedData& data, const std::string& path) {
    std::cerr << "Loading cache from " << path << "..." << std::endl;
    std::ifstream f(path, std::ios::binary);
    if (!f.is_open()) {
        std::cerr << "Error: could not open cache file: " << path << std::endl;
        return false;
    }

    char magic[8];
    f.read(magic, 8);
    if (std::memcmp(magic, CACHE_MAGIC, 8) != 0) {
        std::cerr << "Error: invalid cache magic" << std::endl;
        return false;
    }

    uint32_t version;
    f.read(reinterpret_cast<char*>(&version), sizeof(uint32_t));
    if (version != CACHE_VERSION) {
        std::cerr << "Error: unsupported cache version " << version << std::endl;
        return false;
    }

    uint32_t section_count;
    f.read(reinterpret_cast<char*>(&section_count), sizeof(uint32_t));

    for (uint32_t s = 0; s < section_count; s++) {
        uint32_t type;
        uint64_t length;
        f.read(reinterpret_cast<char*>(&type), sizeof(uint32_t));
        f.read(reinterpret_cast<char*>(&length), sizeof(uint64_t));

        std::vector<char> blob(length);
        f.read(blob.data(), length);

        switch (static_cast<SectionType>(type)) {
        case SectionType::STRING_POOL: {
            if (length < sizeof(uint64_t)) return false;
            uint64_t sz;
            std::memcpy(&sz, blob.data(), sizeof(uint64_t));
            auto& sp = data.string_pool.mutable_data();
            sp.resize(sz);
            if (sz > 0) std::memcpy(sp.data(), blob.data() + sizeof(uint64_t), sz);
            break;
        }
        case SectionType::WAYS:
            if (!deserialize_vector(blob.data(), length, data.ways)) return false;
            break;
        case SectionType::STREET_NODES:
            if (!deserialize_vector(blob.data(), length, data.street_nodes)) return false;
            break;
        case SectionType::CELL_TO_WAYS:
            if (!deserialize_cell_map(blob.data(), length, data.cell_to_ways)) return false;
            break;
        case SectionType::ADDR_POINTS:
            if (!deserialize_vector(blob.data(), length, data.addr_points)) return false;
            break;
        case SectionType::CELL_TO_ADDRS:
            if (!deserialize_cell_map(blob.data(), length, data.cell_to_addrs)) return false;
            break;
        case SectionType::INTERP_WAYS:
            if (!deserialize_vector(blob.data(), length, data.interp_ways)) return false;
            break;
        case SectionType::INTERP_NODES:
            if (!deserialize_vector(blob.data(), length, data.interp_nodes)) return false;
            break;
        case SectionType::CELL_TO_INTERPS:
            if (!deserialize_cell_map(blob.data(), length, data.cell_to_interps)) return false;
            break;
        case SectionType::ADMIN_POLYGONS:
            if (!deserialize_vector(blob.data(), length, data.admin_polygons)) return false;
            break;
        case SectionType::ADMIN_VERTICES:
            if (!deserialize_vector(blob.data(), length, data.admin_vertices)) return false;
            break;
        case SectionType::CELL_TO_ADMIN:
            if (!deserialize_cell_map(blob.data(), length, data.cell_to_admin)) return false;
            break;
        default:
            std::cerr << "Warning: unknown cache section type " << type << ", skipping" << std::endl;
            break;
        }
    }

    std::cerr << "Cache loaded: " << data.ways.size() << " ways, "
              << data.addr_points.size() << " addrs, "
              << data.interp_ways.size() << " interps, "
              << data.admin_polygons.size() << " admin polygons" << std::endl;
    return true;
}

// --- Continent bounding box filtering ---

struct ContinentBBox {
    const char* name;
    double min_lat, max_lat, min_lng, max_lng;


  1300→};
  1301→
  1302→static const ContinentBBox kContinents[] = {
  1303→    {"africa",            -35.0,  37.5,  -25.0,  55.0},
  1304→    {"asia",              -12.0,  82.0,   25.0, 180.0},
  1305→    {"europe",             35.0,  72.0,  -25.0,  45.0},
  1306→    {"north-america",       7.0,  84.0, -170.0, -50.0},
  1307→    {"south-america",     -56.0,  13.0,  -82.0, -34.0},
  1308→    {"oceania",           -50.0,   0.0,  110.0, 180.0},
  1309→    {"central-america",     7.0,  23.5, -120.0, -57.0},
  1310→    {"antarctica",        -90.0, -60.0, -180.0, 180.0},
  1311→};
  1312→
  1313→static bool cell_in_bbox(uint64_t cell_id, const ContinentBBox& bbox) {
  1314→    S2CellId cell(cell_id);
  1315→    S2LatLng center = cell.ToLatLng();
  1316→    double lat = center.lat().degrees();
  1317→    double lng = center.lng().degrees();
  1318→    return lat >= bbox.min_lat && lat <= bbox.max_lat &&
  1319→           lng >= bbox.min_lng && lng <= bbox.max_lng;
  1320→}
  1321→
  1322→static ParsedData filter_by_bbox(const ParsedData& full, const ContinentBBox& bbox) {
  1323→    ParsedData out;
  1324→
  1325→    // --- Filter cell_to_ways and collect referenced way IDs ---
  1326→    std::unordered_set<uint32_t> used_way_ids;
  1327→    for (const auto& [cell_id, ids] : full.cell_to_ways) {
  1328→        if (cell_in_bbox(cell_id, bbox)) {
  1329→            for (uint32_t id : ids) {
  1330→                used_way_ids.insert(id);
  1331→            }
  1332→        }
  1333→    }
  1334→
  1335→    // --- Filter cell_to_addrs and collect referenced addr IDs ---
  1336→    std::unordered_set<uint32_t> used_addr_ids;
  1337→    for (const auto& [cell_id, ids] : full.cell_to_addrs) {
  1338→        if (cell_in_bbox(cell_id, bbox)) {
  1339→            for (uint32_t id : ids) {
  1340→                used_addr_ids.insert(id);
  1341→            }
  1342→        }
  1343→    }
  1344→
  1345→    // --- Filter cell_to_interps and collect referenced interp IDs ---
  1346→    std::unordered_set<uint32_t> used_interp_ids;
  1347→    for (const auto& [cell_id, ids] : full.cell_to_interps) {
  1348→        if (cell_in_bbox(cell_id, bbox)) {
  1349→            for (uint32_t id : ids) {
  1350→                used_interp_ids.insert(id);
  1351→            }
  1352→        }
  1353→    }
  1354→
  1355→    // --- Filter cell_to_admin and collect referenced admin IDs ---
  1356→    std::unordered_set<uint32_t> used_admin_ids;
  1357→    for (const auto& [cell_id, ids] : full.cell_to_admin) {
  1358→        if (cell_in_bbox(cell_id, bbox)) {
  1359→            for (uint32_t id : ids) {
  1360→                used_admin_ids.insert(id & ID_MASK);
  1361→            }
  1362→        }
  1363→    }
  1364→
  1365→    // --- Build remapped ways ---
  1366→    std::unordered_map<uint32_t, uint32_t> way_remap; // old_id -> new_id
  1367→    {
  1368→        std::vector<uint32_t> sorted_ids(used_way_ids.begin(), used_way_ids.end());
  1369→        std::sort(sorted_ids.begin(), sorted_ids.end());
  1370→        for (uint32_t old_id : sorted_ids) {
  1371→            uint32_t new_id = static_cast<uint32_t>(out.ways.size());
  1372→            way_remap[old_id] = new_id;
  1373→            const auto& w = full.ways[old_id];
  1374→            WayHeader nw = w;
  1375→            nw.node_offset = static_cast<uint32_t>(out.street_nodes.size());
  1376→            out.ways.push_back(nw);
  1377→            for (uint8_t n = 0; n < w.node_count; n++) {
  1378→                out.street_nodes.push_back(full.street_nodes[w.node_offset + n]);
  1379→            }
  1380→        }
  1381→    }
  1382→
  1383→    // --- Build remapped addr points ---
  1384→    std::unordered_map<uint32_t, uint32_t> addr_remap;
  1385→    {
  1386→        std::vector<uint32_t> sorted_ids(used_addr_ids.begin(), used_addr_ids.end());
  1387→        std::sort(sorted_ids.begin(), sorted_ids.end());
  1388→        for (uint32_t old_id : sorted_ids) {
  1389→            uint32_t new_id = static_cast<uint32_t>(out.addr_points.size());
  1390→            addr_remap[old_id] = new_id;
  1391→            out.addr_points.push_back(full.addr_points[old_id]);
  1392→        }
  1393→    }
  1394→
  1395→    // --- Build remapped interp ways ---
  1396→    std::unordered_map<uint32_t, uint32_t> interp_remap;
  1397→    {
  1398→        std::vector<uint32_t> sorted_ids(used_interp_ids.begin(), used_interp_ids.end());
  1399→        std::sort(sorted_ids.begin(), sorted_ids.end());
  1400→        for (uint32_t old_id : sorted_ids) {
  1401→            uint32_t new_id = static_cast<uint32_t>(out.interp_ways.size());
  1402→            interp_remap[old_id] = new_id;
  1403→            const auto& iw = full.interp_ways[old_id];
  1404→            InterpWay niw = iw;
  1405→            niw.node_offset = static_cast<uint32_t>(out.interp_nodes.size());
  1406→            out.interp_ways.push_back(niw);
  1407→            for (uint8_t n = 0; n < iw.node_count; n++) {
  1408→                out.interp_nodes.push_back(full.interp_nodes[iw.node_offset + n]);
  1409→            }
  1410→        }
  1411→    }
  1412→
  1413→    // --- Build remapped admin polygons ---
  1414→    std::unordered_map<uint32_t, uint32_t> admin_remap;
  1415→    {
  1416→        std::vector<uint32_t> sorted_ids(used_admin_ids.begin(), used_admin_ids.end());
  1417→        std::sort(sorted_ids.begin(), sorted_ids.end());
  1418→        for (uint32_t old_id : sorted_ids) {
  1419→            uint32_t new_id = static_cast<uint32_t>(out.admin_polygons.size());
  1420→            admin_remap[old_id] = new_id;
  1421→            const auto& ap = full.admin_polygons[old_id];
  1422→            AdminPolygon nap = ap;
  1423→            nap.vertex_offset = static_cast<uint32_t>(out.admin_vertices.size());
  1424→            out.admin_polygons.push_back(nap);
  1425→            for (uint16_t v = 0; v < ap.vertex_count; v++) {
  1426→                out.admin_vertices.push_back(full.admin_vertices[ap.vertex_offset + v]);
  1427→            }
  1428→        }
  1429→    }
  1430→
  1431→    // --- Build remapped cell maps ---
  1432→    for (const auto& [cell_id, ids] : full.cell_to_ways) {
  1433→        if (!cell_in_bbox(cell_id, bbox)) continue;
  1434→        std::vector<uint32_t> new_ids;
  1435→        for (uint32_t id : ids) {
  1436→            auto it = way_remap.find(id);
  1437→            if (it != way_remap.end()) {
  1438→                new_ids.push_back(it->second);
  1439→            }
  1440→        }
  1441→        if (!new_ids.empty()) {
  1442→            out.cell_to_ways[cell_id] = std::move(new_ids);
  1443→        }
  1444→    }
  1445→
  1446→    for (const auto& [cell_id, ids] : full.cell_to_addrs) {
  1447→        if (!cell_in_bbox(cell_id, bbox)) continue;
  1448→        std::vector<uint32_t> new_ids;
  1449→        for (uint32_t id : ids) {
  1450→            auto it = addr_remap.find(id);
  1451→            if (it != addr_remap.end()) {
  1452→                new_ids.push_back(it->second);
  1453→            }
  1454→        }
  1455→        if (!new_ids.empty()) {
  1456→            out.cell_to_addrs[cell_id] = std::move(new_ids);
  1457→        }
  1458→    }
  1459→
  1460→    for (const auto& [cell_id, ids] : full.cell_to_interps) {
  1461→        if (!cell_in_bbox(cell_id, bbox)) continue;
  1462→        std::vector<uint32_t> new_ids;
  1463→        for (uint32_t id : ids) {
  1464→            auto it = interp_remap.find(id);
  1465→            if (it != interp_remap.end()) {
  1466→                new_ids.push_back(it->second);
  1467→            }
  1468→        }
  1469→        if (!new_ids.empty()) {
  1470→            out.cell_to_interps[cell_id] = std::move(new_ids);
  1471→        }
  1472→    }
  1473→
  1474→    for (const auto& [cell_id, ids] : full.cell_to_admin) {
  1475→        if (!cell_in_bbox(cell_id, bbox)) continue;
  1476→        std::vector<uint32_t> new_ids;
  1477→        for (uint32_t id : ids) {
  1478→            uint32_t raw_id = id & ID_MASK;
  1479→            uint32_t flags = id & INTERIOR_FLAG;
  1480→            auto it = admin_remap.find(raw_id);
  1481→            if (it != admin_remap.end()) {
  1482→                new_ids.push_back(it->second | flags);
  1483→            }
  1484→        }
  1485→        if (!new_ids.empty()) {
  1486→            out.cell_to_admin[cell_id] = std::move(new_ids);
  1487→        }
  1488→    }
  1489→
  1490→    // --- Rebuild compact string pool with only referenced strings ---
  1491→    // Collect all referenced string offsets
  1492→    std::unordered_set<uint32_t> used_offsets;
  1493→    for (const auto& w : out.ways) {
  1494→        used_offsets.insert(w.name_id);
  1495→    }
  1496→    for (const auto& a : out.addr_points) {
  1497→        used_offsets.insert(a.housenumber_id);
  1498→        used_offsets.insert(a.street_id);
  1499→    }
  1500→    for (const auto& iw : out.interp_ways) {
  1501→        used_offsets.insert(iw.street_id);
  1502→    }
  1503→    for (const auto& ap : out.admin_polygons) {
  1504→        used_offsets.insert(ap.name_id);
  1505→    }
  1506→
  1507→    // Build remap: old offset -> new offset
  1508→    const auto& old_sp = full.string_pool.data();
  1509→    std::unordered_map<uint32_t, uint32_t> string_remap;
  1510→    auto& new_sp = out.string_pool.mutable_data();
  1511→    new_sp.clear();
  1512→
  1513→    std::vector<uint32_t> sorted_offsets(used_offsets.begin(), used_offsets.end());
  1514→    std::sort(sorted_offsets.begin(), sorted_offsets.end());
  1515→
  1516→    for (uint32_t old_off : sorted_offsets) {
  1517→        uint32_t new_off = static_cast<uint32_t>(new_sp.size());
  1518→        string_remap[old_off] = new_off;
  1519→        // Copy the null-terminated string
  1520→        const char* str = old_sp.data() + old_off;
  1521→        size_t len = std::strlen(str);
  1522→        new_sp.insert(new_sp.end(), str, str + len + 1);
  1523→    }
  1524→
  1525→    // Remap all string IDs in the output data
  1526→    for (auto& w : out.ways) {
  1527→        w.name_id = string_remap[w.name_id];
  1528→    }
  1529→    for (auto& a : out.addr_points) {
  1530→        a.housenumber_id = string_remap[a.housenumber_id];
  1531→        a.street_id = string_remap[a.street_id];
  1532→    }
  1533→    for (auto& iw : out.interp_ways) {
  1534→        iw.street_id = string_remap[iw.street_id];
  1535→    }
  1536→    for (auto& ap : out.admin_polygons) {
  1537→        ap.name_id = string_remap[ap.name_id];
  1538→    }
  1539→
  1540→    return out;
  1541→}
  1542→
  1543→// --- Write cell index ---
  1544→
  1545→static const uint32_t NO_DATA = 0xFFFFFFFFu;
  1546→
  1547→// Write entries file from a hash map (used for addr and interp cell maps).
  1548→static std::vector<uint32_t> write_entries(
  1549→    const std::string& path,
  1550→    const std::vector<uint64_t>& sorted_cells,
  1551→    const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map
  1552→) {
  1553→    struct CellRef { uint64_t cell_id; const std::vector<uint32_t>* ids; };
  1554→    std::vector<CellRef> sorted_refs;
  1555→    sorted_refs.reserve(cell_map.size());
  1556→    for (auto& [id, ids] : cell_map) {
  1557→        sorted_refs.push_back({id, &ids});
  1558→    }
  1559→    std::sort(sorted_refs.begin(), sorted_refs.end(),
  1560→        [](const CellRef& a, const CellRef& b) { return a.cell_id < b.cell_id; });
  1561→
  1562→    std::vector<uint32_t> offsets(sorted_cells.size(), NO_DATA);
  1563→    size_t total_size = 0;
  1564→    for (auto& r : sorted_refs) total_size += sizeof(uint16_t) + r.ids->size() * sizeof(uint32_t);
  1565→
  1566→    std::vector<char> buf;
  1567→    buf.reserve(total_size);
  1568→    uint32_t current = 0;
  1569→    size_t ri = 0;
  1570→    for (uint32_t si = 0; si < sorted_cells.size() && ri < sorted_refs.size(); si++) {
  1571→        if (sorted_cells[si] < sorted_refs[ri].cell_id) continue;
  1572→        if (sorted_cells[si] > sorted_refs[ri].cell_id) { si--; ri++; continue; }
  1573→        offsets[si] = current;
  1574→        const auto& ids = *sorted_refs[ri].ids;
  1575→        uint16_t count = static_cast<uint16_t>(std::min(ids.size(), size_t(65535)));
  1576→        buf.insert(buf.end(), reinterpret_cast<const char*>(&count),
  1577→                   reinterpret_cast<const char*>(&count) + sizeof(count));
  1578→        buf.insert(buf.end(), reinterpret_cast<const char*>(ids.data()),
  1579→                   reinterpret_cast<const char*>(ids.data()) + ids.size() * sizeof(uint32_t));
  1580→        current += sizeof(uint16_t) + ids.size() * sizeof(uint32_t);
  1581→        ri++;
  1582→    }
  1583→    std::ofstream f(path, std::ios::binary);
  1584→    f.write(buf.data(), buf.size());
  1585→    return offsets;
  1586→}
  1587→
  1588→// Write entries file from sorted pairs — parallel chunked merge-join.
  1589→// Splits sorted_cells into chunks, each thread builds its chunk independently,
  1590→// then prefix-sum computes global offsets and buffers are concatenated.
  1591→static std::vector<uint32_t> write_entries_from_sorted(
  1592→    const std::string& path,
  1593→    const std::vector<uint64_t>& sorted_cells,
  1594→    const std::vector<CellItemPair>& sorted_pairs
  1595→) {
  1596→    std::vector<uint32_t> offsets(sorted_cells.size(), NO_DATA);
  1597→    if (sorted_pairs.empty()) {
  1598→        std::ofstream f(path, std::ios::binary);
  1599→        return offsets;

};

static 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},
};

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;
}

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

    // --- Filter cell_to_ways and collect referenced way IDs ---
    std::unordered_set<uint32_t> used_way_ids;
    for (const auto& [cell_id, ids] : full.cell_to_ways) {
        if (cell_in_bbox(cell_id, bbox)) {
            for (uint32_t id : ids) {
                used_way_ids.insert(id);
            }
        }
    }

    // --- Filter cell_to_addrs and collect referenced addr IDs ---
    std::unordered_set<uint32_t> used_addr_ids;
    for (const auto& [cell_id, ids] : full.cell_to_addrs) {
        if (cell_in_bbox(cell_id, bbox)) {
            for (uint32_t id : ids) {
                used_addr_ids.insert(id);
            }
        }
    }

    // --- Filter cell_to_interps and collect referenced interp IDs ---
    std::unordered_set<uint32_t> used_interp_ids;
    for (const auto& [cell_id, ids] : full.cell_to_interps) {
        if (cell_in_bbox(cell_id, bbox)) {
            for (uint32_t id : ids) {
                used_interp_ids.insert(id);
            }
        }
    }

    // --- Filter cell_to_admin and collect referenced admin IDs ---
    std::unordered_set<uint32_t> used_admin_ids;
    for (const auto& [cell_id, ids] : full.cell_to_admin) {
        if (cell_in_bbox(cell_id, bbox)) {
            for (uint32_t id : ids) {
                used_admin_ids.insert(id & ID_MASK);
            }
        }
    }

    // --- Build remapped ways ---
    std::unordered_map<uint32_t, uint32_t> way_remap; // old_id -> new_id
    {
        std::vector<uint32_t> sorted_ids(used_way_ids.begin(), used_way_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        for (uint32_t old_id : sorted_ids) {
            uint32_t new_id = static_cast<uint32_t>(out.ways.size());
            way_remap[old_id] = new_id;
            const auto& w = full.ways[old_id];
            WayHeader nw = w;
            nw.node_offset = static_cast<uint32_t>(out.street_nodes.size());
            out.ways.push_back(nw);
            for (uint8_t n = 0; n < w.node_count; n++) {
                out.street_nodes.push_back(full.street_nodes[w.node_offset + n]);
            }
        }
    }

    // --- Build remapped addr points ---
    std::unordered_map<uint32_t, uint32_t> addr_remap;
    {
        std::vector<uint32_t> sorted_ids(used_addr_ids.begin(), used_addr_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        for (uint32_t old_id : sorted_ids) {
            uint32_t new_id = static_cast<uint32_t>(out.addr_points.size());
            addr_remap[old_id] = new_id;
            out.addr_points.push_back(full.addr_points[old_id]);
        }
    }

    // --- Build remapped interp ways ---
    std::unordered_map<uint32_t, uint32_t> interp_remap;
    {
        std::vector<uint32_t> sorted_ids(used_interp_ids.begin(), used_interp_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        for (uint32_t old_id : sorted_ids) {
            uint32_t new_id = static_cast<uint32_t>(out.interp_ways.size());
            interp_remap[old_id] = new_id;
            const auto& iw = full.interp_ways[old_id];
            InterpWay niw = iw;
            niw.node_offset = static_cast<uint32_t>(out.interp_nodes.size());
            out.interp_ways.push_back(niw);
            for (uint8_t n = 0; n < iw.node_count; n++) {
                out.interp_nodes.push_back(full.interp_nodes[iw.node_offset + n]);
            }
        }
    }

    // --- Build remapped admin polygons ---
    std::unordered_map<uint32_t, uint32_t> admin_remap;
    {
        std::vector<uint32_t> sorted_ids(used_admin_ids.begin(), used_admin_ids.end());
        std::sort(sorted_ids.begin(), sorted_ids.end());
        for (uint32_t old_id : sorted_ids) {
            uint32_t new_id = static_cast<uint32_t>(out.admin_polygons.size());
            admin_remap[old_id] = new_id;
            const auto& ap = full.admin_polygons[old_id];
            AdminPolygon nap = ap;
            nap.vertex_offset = static_cast<uint32_t>(out.admin_vertices.size());
            out.admin_polygons.push_back(nap);
            for (uint16_t v = 0; v < ap.vertex_count; v++) {
                out.admin_vertices.push_back(full.admin_vertices[ap.vertex_offset + v]);
            }
        }
    }

    // --- Build remapped cell maps ---
    for (const auto& [cell_id, ids] : full.cell_to_ways) {
        if (!cell_in_bbox(cell_id, bbox)) continue;
        std::vector<uint32_t> new_ids;
        for (uint32_t id : ids) {
            auto it = way_remap.find(id);
            if (it != way_remap.end()) {
                new_ids.push_back(it->second);
            }
        }
        if (!new_ids.empty()) {
            out.cell_to_ways[cell_id] = std::move(new_ids);
        }
    }

    for (const auto& [cell_id, ids] : full.cell_to_addrs) {
        if (!cell_in_bbox(cell_id, bbox)) continue;
        std::vector<uint32_t> new_ids;
        for (uint32_t id : ids) {
            auto it = addr_remap.find(id);
            if (it != addr_remap.end()) {
                new_ids.push_back(it->second);
            }
        }
        if (!new_ids.empty()) {
            out.cell_to_addrs[cell_id] = std::move(new_ids);
        }
    }

    for (const auto& [cell_id, ids] : full.cell_to_interps) {
        if (!cell_in_bbox(cell_id, bbox)) continue;
        std::vector<uint32_t> new_ids;
        for (uint32_t id : ids) {
            auto it = interp_remap.find(id);
            if (it != interp_remap.end()) {
                new_ids.push_back(it->second);
            }
        }
        if (!new_ids.empty()) {
            out.cell_to_interps[cell_id] = std::move(new_ids);
        }
    }

    for (const auto& [cell_id, ids] : full.cell_to_admin) {
        if (!cell_in_bbox(cell_id, bbox)) continue;
        std::vector<uint32_t> new_ids;
        for (uint32_t id : ids) {
            uint32_t raw_id = id & ID_MASK;
            uint32_t flags = id & INTERIOR_FLAG;
            auto it = admin_remap.find(raw_id);
            if (it != admin_remap.end()) {
                new_ids.push_back(it->second | flags);
            }
        }
        if (!new_ids.empty()) {
            out.cell_to_admin[cell_id] = std::move(new_ids);
        }
    }

    // --- Rebuild compact string pool with only referenced strings ---
    // Collect all referenced string offsets
    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);
    }

    // Build remap: old offset -> new offset
    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;
        // Copy the null-terminated string
        const char* str = old_sp.data() + old_off;
        size_t len = std::strlen(str);
        new_sp.insert(new_sp.end(), str, str + len + 1);
    }

    // Remap all string IDs in the output data
    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;
}

// --- Write cell index ---

static const uint32_t NO_DATA = 0xFFFFFFFFu;

// Write entries file from a hash map (used for addr and interp cell maps).
static std::vector<uint32_t> write_entries(
    const std::string& path,
    const std::vector<uint64_t>& sorted_cells,
    const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map
) {
    struct CellRef { uint64_t cell_id; const std::vector<uint32_t>* ids; };
    std::vector<CellRef> sorted_refs;
    sorted_refs.reserve(cell_map.size());
    for (auto& [id, ids] : cell_map) {
        sorted_refs.push_back({id, &ids});
    }
    std::sort(sorted_refs.begin(), sorted_refs.end(),
        [](const CellRef& a, const CellRef& b) { return a.cell_id < b.cell_id; });

    std::vector<uint32_t> offsets(sorted_cells.size(), NO_DATA);
    size_t total_size = 0;
    for (auto& r : sorted_refs) total_size += sizeof(uint16_t) + r.ids->size() * sizeof(uint32_t);

    std::vector<char> buf;
    buf.reserve(total_size);
    uint32_t current = 0;
    size_t ri = 0;
    for (uint32_t si = 0; si < sorted_cells.size() && ri < sorted_refs.size(); si++) {
        if (sorted_cells[si] < sorted_refs[ri].cell_id) continue;
        if (sorted_cells[si] > sorted_refs[ri].cell_id) { si--; ri++; continue; }
        offsets[si] = current;
        const auto& ids = *sorted_refs[ri].ids;
        uint16_t count = static_cast<uint16_t>(std::min(ids.size(), size_t(65535)));
        buf.insert(buf.end(), reinterpret_cast<const char*>(&count),
                   reinterpret_cast<const char*>(&count) + sizeof(count));
        buf.insert(buf.end(), reinterpret_cast<const char*>(ids.data()),
                   reinterpret_cast<const char*>(ids.data()) + ids.size() * sizeof(uint32_t));
        current += sizeof(uint16_t) + ids.size() * sizeof(uint32_t);
        ri++;
    }
    std::ofstream f(path, std::ios::binary);
    f.write(buf.data(), buf.size());
    return offsets;
}

// Write entries file from sorted pairs — parallel chunked merge-join.
// Splits sorted_cells into chunks, each thread builds its chunk independently,
// then prefix-sum computes global offsets and buffers are concatenated.
static std::vector<uint32_t> write_entries_from_sorted(
    const std::string& path,
    const std::vector<uint64_t>& sorted_cells,
    const std::vector<CellItemPair>& sorted_pairs
) {
    std::vector<uint32_t> offsets(sorted_cells.size(), NO_DATA);
    if (sorted_pairs.empty()) {
        std::ofstream f(path, std::ios::binary);
        return offsets;


Let me continue reading the rest of the file to understand the full codebase before starting the refactor.