   774→// --- Cache serialization ---
   775→
   776→enum class SectionType : uint32_t {
   777→    STRING_POOL = 0,
   778→    WAYS = 1,
   779→    STREET_NODES = 2,
   780→    CELL_TO_WAYS = 3,
   781→    ADDR_POINTS = 4,
   782→    CELL_TO_ADDRS = 5,
   783→    INTERP_WAYS = 6,
   784→    INTERP_NODES = 7,
   785→    CELL_TO_INTERPS = 8,
   786→    ADMIN_POLYGONS = 9,
   787→    ADMIN_VERTICES = 10,
   788→    CELL_TO_ADMIN = 11,
   789→};
   790→
   791→static const char CACHE_MAGIC[8] = {'T','G','C','A','C','H','E','\0'};
   792→static const uint32_t CACHE_VERSION = 1;
   793→static const uint32_t CACHE_SECTION_COUNT = 12;
   794→
   795→// Serialize a vector of structs to a binary blob
   796→template<typename T>
   797→static std::vector<char> serialize_vector(const std::vector<T>& vec) {
   798→    std::vector<char> buf;
   799→    uint64_t count = vec.size();
   800→    buf.resize(sizeof(uint64_t) + count * sizeof(T));
   801→    std::memcpy(buf.data(), &count, sizeof(uint64_t));
   802→    if (count > 0) {
   803→        std::memcpy(buf.data() + sizeof(uint64_t), vec.data(), count * sizeof(T));
   804→    }
   805→    return buf;
   806→}
   807→
   808→// Deserialize a vector of structs from a binary blob
   809→template<typename T>
   810→static bool deserialize_vector(const char* data, uint64_t length, std::vector<T>& vec) {
   811→    if (length < sizeof(uint64_t)) return false;
   812→    uint64_t count;
   813→    std::memcpy(&count, data, sizeof(uint64_t));
   814→    if (length < sizeof(uint64_t) + count * sizeof(T)) return false;
   815→    vec.resize(count);
   816→    if (count > 0) {
   817→        std::memcpy(vec.data(), data + sizeof(uint64_t), count * sizeof(T));
   818→    }
   819→    return true;
   820→}
   821→
   822→// Serialize a cell map (uint64_t -> vector<uint32_t>)
   823→static std::vector<char> serialize_cell_map(const std::unordered_map<uint64_t, std::vector<uint32_t>>& map) {
   824→    // Calculate total size
   825→    size_t total = sizeof(uint64_t); // entry count
   826→    for (const auto& [cell_id, ids] : map) {
   827→        total += sizeof(uint64_t) + sizeof(uint32_t) + ids.size() * sizeof(uint32_t);
   828→    }
   829→    std::vector<char> buf(total);
   830→    char* ptr = buf.data();
   831→
   832→    uint64_t entry_count = map.size();
   833→    std::memcpy(ptr, &entry_count, sizeof(uint64_t));
   834→    ptr += sizeof(uint64_t);
   835→
   836→    for (const auto& [cell_id, ids] : map) {
   837→        std::memcpy(ptr, &cell_id, sizeof(uint64_t));
   838→        ptr += sizeof(uint64_t);
   839→        uint32_t id_count = static_cast<uint32_t>(ids.size());
   840→        std::memcpy(ptr, &id_count, sizeof(uint32_t));
   841→        ptr += sizeof(uint32_t);
   842→        if (id_count > 0) {
   843→            std::memcpy(ptr, ids.data(), id_count * sizeof(uint32_t));
   844→            ptr += id_count * sizeof(uint32_t);
   845→        }
   846→    }
   847→    return buf;
   848→}
   849→
   850→// Deserialize a cell map
   851→static bool deserialize_cell_map(const char* data, uint64_t length,
   852→                                  std::unordered_map<uint64_t, std::vector<uint32_t>>& map) {
   853→    if (length < sizeof(uint64_t)) return false;
   854→    const char* ptr = data;
   855→    const char* end = data + length;
   856→
   857→    uint64_t entry_count;
   858→    std::memcpy(&entry_count, ptr, sizeof(uint64_t));
   859→    ptr += sizeof(uint64_t);
   860→
   861→    map.reserve(entry_count);
   862→    for (uint64_t i = 0; i < entry_count; i++) {
   863→        if (ptr + sizeof(uint64_t) + sizeof(uint32_t) > end) return false;
   864→        uint64_t cell_id;
   865→        std::memcpy(&cell_id, ptr, sizeof(uint64_t));
   866→        ptr += sizeof(uint64_t);
   867→        uint32_t id_count;
   868→        std::memcpy(&id_count, ptr, sizeof(uint32_t));
   869→        ptr += sizeof(uint32_t);
   870→        if (ptr + id_count * sizeof(uint32_t) > end) return false;
   871→        std::vector<uint32_t> ids(id_count);
   872→        if (id_count > 0) {
   873→            std::memcpy(ids.data(), ptr, id_count * sizeof(uint32_t));
   874→            ptr += id_count * sizeof(uint32_t);
   875→        }
   876→        map[cell_id] = std::move(ids);
   877→    }
   878→    return true;
   879→}
   880→
   881→static void serialize_cache(const ParsedData& data, const std::string& path) {
   882→    std::cerr << "Saving cache to " << path << "..." << std::endl;
   883→    std::ofstream f(path, std::ios::binary);
   884→    if (!f.is_open()) {
   885→        std::cerr << "Error: could not open cache file for writing: " << path << std::endl;
   886→        return;
   887→    }
   888→
   889→    f.write(CACHE_MAGIC, 8);
   890→    f.write(reinterpret_cast<const char*>(&CACHE_VERSION), sizeof(uint32_t));
   891→    f.write(reinterpret_cast<const char*>(&CACHE_SECTION_COUNT), sizeof(uint32_t));
   892→
   893→    // Helper to write a section
   894→    auto write_section = [&](SectionType type, const std::vector<char>& blob) {
   895→        uint32_t t = static_cast<uint32_t>(type);
   896→        uint64_t len = blob.size();
   897→        f.write(reinterpret_cast<const char*>(&t), sizeof(uint32_t));
   898→        f.write(reinterpret_cast<const char*>(&len), sizeof(uint64_t));
   899→        f.write(blob.data(), len);
   900→    };
   901→
   902→    // STRING_POOL: raw char vector
   903→    {
   904→        const auto& sp = data.string_pool.data();
   905→        std::vector<char> blob(sizeof(uint64_t) + sp.size());
   906→        uint64_t sz = sp.size();
   907→        std::memcpy(blob.data(), &sz, sizeof(uint64_t));
   908→        if (sz > 0) std::memcpy(blob.data() + sizeof(uint64_t), sp.data(), sz);
   909→        write_section(SectionType::STRING_POOL, blob);
   910→    }
   911→
   912→    write_section(SectionType::WAYS, serialize_vector(data.ways));
   913→    write_section(SectionType::STREET_NODES, serialize_vector(data.street_nodes));
   914→    write_section(SectionType::CELL_TO_WAYS, serialize_cell_map(data.cell_to_ways));
   915→    write_section(SectionType::ADDR_POINTS, serialize_vector(data.addr_points));
   916→    write_section(SectionType::CELL_TO_ADDRS, serialize_cell_map(data.cell_to_addrs));
   917→    write_section(SectionType::INTERP_WAYS, serialize_vector(data.interp_ways));
   918→    write_section(SectionType::INTERP_NODES, serialize_vector(data.interp_nodes));
   919→    write_section(SectionType::CELL_TO_INTERPS, serialize_cell_map(data.cell_to_interps));
   920→    write_section(SectionType::ADMIN_POLYGONS, serialize_vector(data.admin_polygons));
   921→    write_section(SectionType::ADMIN_VERTICES, serialize_vector(data.admin_vertices));
   922→    write_section(SectionType::CELL_TO_ADMIN, serialize_cell_map(data.cell_to_admin));
   923→
   924→    std::cerr << "Cache saved." << std::endl;
   925→}
   926→
   927→static bool deserialize_cache(ParsedData& data, const std::string& path) {
   928→    std::cerr << "Loading cache from " << path << "..." << std::endl;
   929→    std::ifstream f(path, std::ios::binary);
   930→    if (!f.is_open()) {
   931→        std::cerr << "Error: could not open cache file: " << path << std::endl;
   932→        return false;
   933→    }
   934→
   935→    char magic[8];
   936→    f.read(magic, 8);
   937→    if (std::memcmp(magic, CACHE_MAGIC, 8) != 0) {
   938→        std::cerr << "Error: invalid cache magic" << std::endl;
   939→        return false;
   940→    }
   941→
   942→    uint32_t version;
   943→    f.read(reinterpret_cast<char*>(&version), sizeof(uint32_t));
   944→    if (version != CACHE_VERSION) {
   945→        std::cerr << "Error: unsupported cache version " << version << std::endl;
   946→        return false;
   947→    }
   948→
   949→    uint32_t section_count;
   950→    f.read(reinterpret_cast<char*>(&section_count), sizeof(uint32_t));
   951→
   952→    for (uint32_t s = 0; s < section_count; s++) {
   953→        uint32_t type;
   954→        uint64_t length;
   955→        f.read(reinterpret_cast<char*>(&type), sizeof(uint32_t));
   956→        f.read(reinterpret_cast<char*>(&length), sizeof(uint64_t));
   957→
   958→        std::vector<char> blob(length);
   959→        f.read(blob.data(), length);
   960→
   961→        switch (static_cast<SectionType>(type)) {
   962→        case SectionType::STRING_POOL: {
   963→            if (length < sizeof(uint64_t)) return false;
   964→            uint64_t sz;
   965→            std::memcpy(&sz, blob.data(), sizeof(uint64_t));
   966→            auto& sp = data.string_pool.mutable_data();
   967→            sp.resize(sz);
   968→            if (sz > 0) std::memcpy(sp.data(), blob.data() + sizeof(uint64_t), sz);
   969→            break;
   970→        }
   971→        case SectionType::WAYS:
   972→            if (!deserialize_vector(blob.data(), length, data.ways)) return false;
   973→            break;
   974→        case SectionType::STREET_NODES:
   975→            if (!deserialize_vector(blob.data(), length, data.street_nodes)) return false;
   976→            break;
   977→        case SectionType::CELL_TO_WAYS:
   978→            if (!deserialize_cell_map(blob.data(), length, data.cell_to_ways)) return false;
   979→            break;
   980→        case SectionType::ADDR_POINTS:
   981→            if (!deserialize_vector(blob.data(), length, data.addr_points)) return false;
   982→            break;
   983→        case SectionType::CELL_TO_ADDRS:
   984→            if (!deserialize_cell_map(blob.data(), length, data.cell_to_addrs)) return false;
   985→            break;
   986→        case SectionType::INTERP_WAYS:
   987→            if (!deserialize_vector(blob.data(), length, data.interp_ways)) return false;
   988→            break;
   989→        case SectionType::INTERP_NODES:
   990→            if (!deserialize_vector(blob.data(), length, data.interp_nodes)) return false;
   991→            break;
   992→        case SectionType::CELL_TO_INTERPS:
   993→            if (!deserialize_cell_map(blob.data(), length, data.cell_to_interps)) return false;
   994→            break;
   995→        case SectionType::ADMIN_POLYGONS:
   996→            if (!deserialize_vector(blob.data(), length, data.admin_polygons)) return false;
   997→            break;
   998→        case SectionType::ADMIN_VERTICES:
   999→            if (!deserialize_vector(blob.data(), length, data.admin_vertices)) return false;
  1000→            break;
  1001→        case SectionType::CELL_TO_ADMIN:
  1002→            if (!deserialize_cell_map(blob.data(), length, data.cell_to_admin)) return false;
  1003→            break;
  1004→        default:
  1005→            std::cerr << "Warning: unknown cache section type " << type << ", skipping" << std::endl;
  1006→            break;
  1007→        }
  1008→    }
  1009→
  1010→    std::cerr << "Cache loaded: " << data.ways.size() << " ways, "
  1011→              << data.addr_points.size() << " addrs, "
  1012→              << data.interp_ways.size() << " interps, "
  1013→              << data.admin_polygons.size() << " admin polygons" << std::endl;
  1014→    return true;
  1015→}
  1016→
  1017→// --- Continent bounding box filtering ---
  1018→
  1019→struct ContinentBBox {
  1020→    const char* name;
  1021→    double min_lat, max_lat, min_lng, max_lng;
  1022→};
  1023→

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


Write

#include "cache.h"

#include <cstring>
#include <fstream>
#include <iostream>

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;

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

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

static std::vector<char> serialize_cell_map(const std::unordered_map<uint64_t, std::vector<uint32_t>>& map) {
    size_t total = sizeof(uint64_t);
    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;
}

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

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

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


File created successfully at: /root/traccar-geocoder/builder/src/cache.cpp

#include "cache.h"

#include <cstring>
#include <fstream>
#include <iostream>

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;

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

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

static std::vector<char> serialize_cell_map(const std::unordered_map<uint64_t, std::vector<uint32_t>>& map) {
    size_t total = sizeof(uint64_t);
    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;
}

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

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

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


Now continent_filter and cell_index. These are the two biggest remaining pieces:


#pragma once

#include "parsed_data.h"

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

extern const ContinentBBox kContinents[];
extern const size_t kContinentCount;

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


File created successfully at: /root/traccar-geocoder/builder/src/continent_filter.h

#pragma once

#include "parsed_data.h"

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

extern const ContinentBBox kContinents[];
extern const size_t kContinentCount;

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


#pragma once

#include <string>
#include <unordered_map>
#include <vector>

#include "types.h"
#include "parsed_data.h"

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

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

void write_cell_index(
    const std::string& cells_path,
    const std::string& entries_path,
    const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map);

void write_index(const ParsedData& data, const std::string& output_dir, IndexMode mode);


File created successfully at: /root/traccar-geocoder/builder/src/cell_index.h

#pragma once

#include <string>
#include <unordered_map>
#include <vector>

#include "types.h"
#include "parsed_data.h"

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

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

void write_cell_index(
    const std::string& cells_path,
    const std::string& entries_path,
    const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map);

void write_index(const ParsedData& data, const std::string& output_dir, IndexMode mode);


Now I need to create continent_filter.cpp and cell_index.cpp. Let me read those sections from build_index.cpp:


  1017→// --- Continent bounding box filtering ---
  1018→
  1019→struct ContinentBBox {
  1020→    const char* name;
  1021→    double min_lat, max_lat, min_lng, max_lng;
  1022→};
  1023→
  1024→static const ContinentBBox kContinents[] = {
  1025→    {"africa",            -35.0,  37.5,  -25.0,  55.0},
  1026→    {"asia",              -12.0,  82.0,   25.0, 180.0},

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

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

static const ContinentBBox kContinents[] = {
    {"africa",            -35.0,  37.5,  -25.0,  55.0},
    {"asia",              -12.0,  82.0,   25.0, 180.0},


#include "continent_filter.h"

#include <algorithm>
#include <cstring>
#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::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);
        }
    }

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

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

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

    // Remap ways
    std::unordered_map<uint32_t, uint32_t> way_remap;
    { 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]);
      } }

    // Remap addrs
    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]);
      } }

    // Remap interps
    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]);
      } }

    // Remap admins
    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]);
      } }

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


File created successfully at: /root/traccar-geocoder/builder/src/continent_filter.cpp

#include "continent_filter.h"

#include <algorithm>
#include <cstring>
#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::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);
        }
    }

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

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

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

    // Remap ways
    std::unordered_map<uint32_t, uint32_t> way_remap;
    { 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]);
      } }

    // Remap addrs
    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]);
      } }

    // Remap interps
    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]);
      } }

    // Remap admins
    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]);
      } }

    // Remap 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
    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 cell_index.cpp — this is the index writing module. Let me read the relevant section:


  1265→// --- Write cell index ---
  1266→
  1267→// NO_DATA is in types.h
  1268→
  1269→// Write entries file from a hash map (used for addr and interp cell maps).

// --- Write cell index ---

// NO_DATA is in types.h

// Write entries file from a hash map (used for addr and interp cell maps).


  1269→// Write entries file from a hash map (used for addr and interp cell maps).
  1270→static std::vector<uint32_t> write_entries(
  1271→    const std::string& path,
  1272→    const std::vector<uint64_t>& sorted_cells,
  1273→    const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map
  1274→) {
  1275→    struct CellRef { uint64_t cell_id; const std::vector<uint32_t>* ids; };
  1276→    std::vector<CellRef> sorted_refs;
  1277→    sorted_refs.reserve(cell_map.size());
  1278→    for (auto& [id, ids] : cell_map) {
  1279→        sorted_refs.push_back({id, &ids});
  1280→    }
  1281→    std::sort(sorted_refs.begin(), sorted_refs.end(),
  1282→        [](const CellRef& a, const CellRef& b) { return a.cell_id < b.cell_id; });
  1283→
  1284→    std::vector<uint32_t> offsets(sorted_cells.size(), NO_DATA);
  1285→    size_t total_size = 0;
  1286→    for (auto& r : sorted_refs) total_size += sizeof(uint16_t) + r.ids->size() * sizeof(uint32_t);
  1287→
  1288→    std::vector<char> buf;
  1289→    buf.reserve(total_size);
  1290→    uint32_t current = 0;
  1291→    size_t ri = 0;
  1292→    for (uint32_t si = 0; si < sorted_cells.size() && ri < sorted_refs.size(); si++) {
  1293→        if (sorted_cells[si] < sorted_refs[ri].cell_id) continue;
  1294→        if (sorted_cells[si] > sorted_refs[ri].cell_id) { si--; ri++; continue; }
  1295→        offsets[si] = current;
  1296→        const auto& ids = *sorted_refs[ri].ids;
  1297→        uint16_t count = static_cast<uint16_t>(std::min(ids.size(), size_t(65535)));
  1298→        buf.insert(buf.end(), reinterpret_cast<const char*>(&count),
  1299→                   reinterpret_cast<const char*>(&count) + sizeof(count));
  1300→        buf.insert(buf.end(), reinterpret_cast<const char*>(ids.data()),
  1301→                   reinterpret_cast<const char*>(ids.data()) + ids.size() * sizeof(uint32_t));
  1302→        current += sizeof(uint16_t) + ids.size() * sizeof(uint32_t);
  1303→        ri++;
  1304→    }
  1305→    std::ofstream f(path, std::ios::binary);
  1306→    f.write(buf.data(), buf.size());
  1307→    return offsets;
  1308→}
  1309→
  1310→// Write entries file from sorted pairs — parallel chunked merge-join.
  1311→// Splits sorted_cells into chunks, each thread builds its chunk independently,
  1312→// then prefix-sum computes global offsets and buffers are concatenated.
  1313→static std::vector<uint32_t> write_entries_from_sorted(
  1314→    const std::string& path,
  1315→    const std::vector<uint64_t>& sorted_cells,
  1316→    const std::vector<CellItemPair>& sorted_pairs
  1317→) {
  1318→    std::vector<uint32_t> offsets(sorted_cells.size(), NO_DATA);
  1319→    if (sorted_pairs.empty()) {
  1320→        std::ofstream f(path, std::ios::binary);
  1321→        return offsets;
  1322→    }
  1323→
  1324→    // Find where each chunk of sorted_cells starts in sorted_pairs
  1325→    // using binary search (both are sorted by cell_id)
  1326→    unsigned int nthreads = std::min(std::thread::hardware_concurrency(), 32u);
  1327→    if (nthreads == 0) nthreads = 4;
  1328→    size_t cells_per_chunk = (sorted_cells.size() + nthreads - 1) / nthreads;
  1329→
  1330→    struct ChunkResult {
  1331→        std::vector<char> buf;
  1332→        size_t cell_start, cell_end; // range in sorted_cells
  1333→        uint32_t local_size; // total bytes in this chunk's buffer
  1334→    };
  1335→    std::vector<ChunkResult> chunks(nthreads);
  1336→
  1337→    // Parallel: each thread processes its range of sorted_cells
  1338→    std::vector<std::thread> threads;
  1339→    for (unsigned int t = 0; t < nthreads; t++) {
  1340→        size_t cs = t * cells_per_chunk;
  1341→        size_t ce = std::min(cs + cells_per_chunk, sorted_cells.size());
  1342→        if (cs >= sorted_cells.size()) break;
  1343→
  1344→        threads.emplace_back([&, t, cs, ce]() {
  1345→            auto& chunk = chunks[t];
  1346→            chunk.cell_start = cs;
  1347→            chunk.cell_end = ce;
  1348→            chunk.local_size = 0;
  1349→
  1350→            // Binary search for where this chunk's first cell appears in sorted_pairs
  1351→            size_t pi = std::lower_bound(sorted_pairs.begin(), sorted_pairs.end(),
  1352→                sorted_cells[cs], [](const CellItemPair& p, uint64_t id) {
  1353→                    return p.cell_id < id;
  1354→                }) - sorted_pairs.begin();
  1355→
  1356→            // Merge-join for this chunk
  1357→            for (size_t si = cs; si < ce && pi < sorted_pairs.size(); si++) {
  1358→                if (sorted_cells[si] < sorted_pairs[pi].cell_id) continue;
  1359→                while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id < sorted_cells[si]) pi++;
  1360→                if (pi >= sorted_pairs.size() || sorted_pairs[pi].cell_id != sorted_cells[si]) continue;
  1361→
  1362→                offsets[si] = chunk.local_size; // local offset, adjusted later
  1363→                size_t start = pi;
  1364→                while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id == sorted_cells[si]) pi++;
  1365→                uint16_t count = static_cast<uint16_t>(std::min(pi - start, size_t(65535)));
  1366→                size_t entry_size = sizeof(uint16_t) + (pi - start) * sizeof(uint32_t);
  1367→                size_t buf_pos = chunk.buf.size();
  1368→                chunk.buf.resize(buf_pos + entry_size);
  1369→                memcpy(chunk.buf.data() + buf_pos, &count, sizeof(count));
  1370→                for (size_t k = start; k < pi; k++) {
  1371→                    memcpy(chunk.buf.data() + buf_pos + sizeof(uint16_t) + (k - start) * sizeof(uint32_t),
  1372→                           &sorted_pairs[k].item_id, sizeof(uint32_t));
  1373→                }
  1374→                chunk.local_size += entry_size;
  1375→            }
  1376→        });
  1377→    }
  1378→    for (auto& t : threads) t.join();
  1379→
  1380→    // Prefix-sum: compute global offsets for each chunk
  1381→    uint32_t global_offset = 0;
  1382→    for (auto& chunk : chunks) {
  1383→        for (size_t si = chunk.cell_start; si < chunk.cell_end; si++) {
  1384→            if (offsets[si] != NO_DATA) {
  1385→                offsets[si] += global_offset;
  1386→            }
  1387→        }
  1388→        global_offset += chunk.local_size;
  1389→    }
  1390→
  1391→    // Concatenate buffers and write
  1392→    std::vector<char> buf;
  1393→    buf.reserve(global_offset);
  1394→    for (auto& chunk : chunks) {
  1395→        buf.insert(buf.end(), chunk.buf.begin(), chunk.buf.end());
  1396→    }
  1397→
  1398→    std::ofstream f(path, std::ios::binary);
  1399→    f.write(buf.data(), buf.size());
  1400→    return offsets;
  1401→}
  1402→
  1403→static void write_cell_index(
  1404→    const std::string& cells_path,
  1405→    const std::string& entries_path,
  1406→    const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map
  1407→) {
  1408→    std::vector<std::pair<uint64_t, std::vector<uint32_t>>> sorted(
  1409→        cell_map.begin(), cell_map.end());
  1410→    std::sort(sorted.begin(), sorted.end());
  1411→
  1412→    {
  1413→        std::ofstream f(cells_path, std::ios::binary);
  1414→        uint32_t current_offset = 0;
  1415→        for (const auto& [cell_id, ids] : sorted) {
  1416→            f.write(reinterpret_cast<const char*>(&cell_id), sizeof(cell_id));
  1417→            f.write(reinterpret_cast<const char*>(&current_offset), sizeof(current_offset));
  1418→            current_offset += sizeof(uint16_t) + ids.size() * sizeof(uint32_t);
  1419→        }
  1420→    }
  1421→
  1422→    {
  1423→        std::ofstream f(entries_path, std::ios::binary);
  1424→        for (const auto& [cell_id, ids] : sorted) {
  1425→            uint16_t count = static_cast<uint16_t>(std::min(ids.size(), size_t(65535)));
  1426→            f.write(reinterpret_cast<const char*>(&count), sizeof(count));
  1427→            f.write(reinterpret_cast<const char*>(ids.data()), ids.size() * sizeof(uint32_t));
  1428→        }
  1429→    }
  1430→}
  1431→
  1432→// --- Write all index files ---
  1433→
  1434→static void write_index(const ParsedData& data, const std::string& output_dir, IndexMode mode) {
  1435→    ensure_dir(output_dir);
  1436→    auto _wt = std::chrono::steady_clock::now();
  1437→
  1438→    bool write_streets = (mode != IndexMode::AdminOnly);
  1439→    bool write_addresses = (mode == IndexMode::Full);
  1440→
  1441→    if (write_streets) {
  1442→        // Build sorted_geo_cells from sorted pairs where available (avoids hash map iteration).
  1443→        // Extract unique cell IDs from each sorted pair vector in parallel, then merge.
  1444→        std::vector<uint64_t> sorted_geo_cells;
  1445→        {
  1446→            auto extract_unique_cells = [](const std::vector<CellItemPair>& pairs) {
  1447→                std::vector<uint64_t> cells;
  1448→                cells.reserve(pairs.size() / 2);
  1449→                for (size_t i = 0; i < pairs.size(); ) {
  1450→                    cells.push_back(pairs[i].cell_id);
  1451→                    uint64_t cur = pairs[i].cell_id;
  1452→                    while (i < pairs.size() && pairs[i].cell_id == cur) i++;
  1453→                }
  1454→                return cells;
  1455→            };
  1456→            auto extract_from_map = [](const std::unordered_map<uint64_t, std::vector<uint32_t>>& m) {
  1457→                std::vector<uint64_t> cells;
  1458→                cells.reserve(m.size());
  1459→                for (auto& [id, _] : m) cells.push_back(id);
  1460→                std::sort(cells.begin(), cells.end());
  1461→                return cells;
  1462→            };
  1463→
  1464→            // Extract unique cell IDs in parallel
  1465→            std::vector<uint64_t> way_cells, addr_cells, interp_cells;
  1466→            {
  1467→                auto f1 = std::async(std::launch::async, [&] {
  1468→                    return !data.sorted_way_cells.empty()
  1469→                        ? extract_unique_cells(data.sorted_way_cells)
  1470→                        : extract_from_map(data.cell_to_ways);
  1471→                });
  1472→                if (write_addresses) {
  1473→                    auto f2 = std::async(std::launch::async, [&] {
  1474→                        return !data.sorted_addr_cells.empty()
  1475→                            ? extract_unique_cells(data.sorted_addr_cells)
  1476→                            : extract_from_map(data.cell_to_addrs);
  1477→                    });
  1478→                    auto f3 = std::async(std::launch::async, [&] {
  1479→                        return extract_from_map(data.cell_to_interps);
  1480→                    });
  1481→                    addr_cells = f2.get();
  1482→                    interp_cells = f3.get();
  1483→                }
  1484→                way_cells = f1.get();
  1485→            }
  1486→
  1487→            // Merge sorted unique cell ID vectors
  1488→            sorted_geo_cells.reserve(way_cells.size() + addr_cells.size() + interp_cells.size());
  1489→            std::merge(way_cells.begin(), way_cells.end(),
  1490→                       addr_cells.begin(), addr_cells.end(),
  1491→                       std::back_inserter(sorted_geo_cells));
  1492→            if (!interp_cells.empty()) {
  1493→                std::vector<uint64_t> tmp;
  1494→                tmp.reserve(sorted_geo_cells.size() + interp_cells.size());
  1495→                std::merge(sorted_geo_cells.begin(), sorted_geo_cells.end(),
  1496→                           interp_cells.begin(), interp_cells.end(),
  1497→                           std::back_inserter(tmp));
  1498→                sorted_geo_cells = std::move(tmp);
  1499→            }
  1500→            // Deduplicate (merge may have duplicates from overlapping cells)
  1501→            sorted_geo_cells.erase(std::unique(sorted_geo_cells.begin(), sorted_geo_cells.end()),
  1502→                                    sorted_geo_cells.end());
  1503→        }
  1504→
  1505→        // Write entry files in parallel (returns positional offset vectors, not hash maps)
  1506→        std::vector<uint32_t> street_offsets, addr_offsets, interp_offsets;
  1507→        {
  1508→            auto f1 = std::async(std::launch::async, [&]() {
  1509→                // Use direct sorted-pair writing for ways (skips hash map extraction + sort)
  1510→                if (!data.sorted_way_cells.empty()) {
  1511→                    return write_entries_from_sorted(output_dir + "/street_entries.bin", sorted_geo_cells, data.sorted_way_cells);
  1512→                }
  1513→                return write_entries(output_dir + "/street_entries.bin", sorted_geo_cells, data.cell_to_ways);
  1514→            });
  1515→            if (write_addresses) {
  1516→                auto f2 = std::async(std::launch::async, [&]() {
  1517→                    if (!data.sorted_addr_cells.empty()) {
  1518→                        return write_entries_from_sorted(output_dir + "/addr_entries.bin", sorted_geo_cells, data.sorted_addr_cells);
  1519→                    }
  1520→                    return write_entries(output_dir + "/addr_entries.bin", sorted_geo_cells, data.cell_to_addrs);
  1521→                });
  1522→                auto f3 = std::async(std::launch::async, [&]() {
  1523→                    return write_entries(output_dir + "/interp_entries.bin", sorted_geo_cells, data.cell_to_interps);
  1524→                });
  1525→                addr_offsets = f2.get();
  1526→                interp_offsets = f3.get();
  1527→            }
  1528→            street_offsets = f1.get();
  1529→        }
  1530→
  1531→        log_phase("  Write: entry files", _wt);
  1532→        // Build geo_cells.bin — parallel buffer fill + single write
  1533→        {
  1534→            size_t n = sorted_geo_cells.size();
  1535→            size_t row_size = sizeof(uint64_t) + 3 * sizeof(uint32_t);
  1536→            std::vector<char> buf(n * row_size);
  1537→
  1538→            if (addr_offsets.empty()) addr_offsets.resize(n, NO_DATA);
  1539→            if (interp_offsets.empty()) interp_offsets.resize(n, NO_DATA);
  1540→
  1541→            // Parallel buffer fill — split into chunks across threads
  1542→            unsigned int nthreads = std::thread::hardware_concurrency();
  1543→            if (nthreads == 0) nthreads = 4;
  1544→            size_t chunk = (n + nthreads - 1) / nthreads;
  1545→            std::vector<std::thread> fill_threads;
  1546→            for (unsigned int t = 0; t < nthreads; t++) {
  1547→                size_t start = t * chunk;
  1548→                size_t end = std::min(start + chunk, n);
  1549→                if (start >= n) break;
  1550→                fill_threads.emplace_back([&, start, end]() {
  1551→                    char* ptr = buf.data() + start * row_size;
  1552→                    for (size_t i = start; i < end; i++) {
  1553→                        memcpy(ptr, &sorted_geo_cells[i], sizeof(uint64_t)); ptr += sizeof(uint64_t);
  1554→                        memcpy(ptr, &street_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
  1555→                        memcpy(ptr, &addr_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
  1556→                        memcpy(ptr, &interp_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
  1557→                    }
  1558→                });
  1559→            }
  1560→            for (auto& t : fill_threads) t.join();
  1561→
  1562→            std::ofstream f(output_dir + "/geo_cells.bin", std::ios::binary);
  1563→            f.write(buf.data(), buf.size());
  1564→        }
  1565→
  1566→        std::cerr << "geo index: " << sorted_geo_cells.size() << " cells ("
  1567→                  << data.ways.size() << " ways, "
  1568→                  << data.addr_points.size() << " addrs, "
  1569→                  << data.interp_ways.size() << " interps)" << std::endl;
  1570→    }
  1571→
  1572→    log_phase("  Write: geo_cells.bin", _wt);
  1573→    // Write admin cell index, raw data files, and admin polygons in parallel
  1574→    auto admin_future = std::async(std::launch::async, [&] {
  1575→        write_cell_index(output_dir + "/admin_cells.bin", output_dir + "/admin_entries.bin", data.cell_to_admin);
  1576→        std::cerr << "admin index: " << data.cell_to_admin.size() << " cells, " << data.admin_polygons.size() << " polygons" << std::endl;
  1577→    });
  1578→
  1579→    std::vector<std::future<void>> write_futures;
  1580→
  1581→    if (write_streets) {
  1582→        write_futures.push_back(std::async(std::launch::async, [&] {
  1583→            std::ofstream f(output_dir + "/street_ways.bin", std::ios::binary);
  1584→            f.write(reinterpret_cast<const char*>(data.ways.data()), data.ways.size() * sizeof(WayHeader));
  1585→        }));
  1586→        write_futures.push_back(std::async(std::launch::async, [&] {
  1587→            std::ofstream f(output_dir + "/street_nodes.bin", std::ios::binary);
  1588→            f.write(reinterpret_cast<const char*>(data.street_nodes.data()), data.street_nodes.size() * sizeof(NodeCoord));
  1589→        }));
  1590→        if (write_addresses) {
  1591→            write_futures.push_back(std::async(std::launch::async, [&] {
  1592→                std::ofstream f(output_dir + "/addr_points.bin", std::ios::binary);
  1593→                f.write(reinterpret_cast<const char*>(data.addr_points.data()), data.addr_points.size() * sizeof(AddrPoint));
  1594→            }));
  1595→            write_futures.push_back(std::async(std::launch::async, [&] {
  1596→                std::ofstream f(output_dir + "/interp_ways.bin", std::ios::binary);
  1597→                f.write(reinterpret_cast<const char*>(data.interp_ways.data()), data.interp_ways.size() * sizeof(InterpWay));
  1598→            }));
  1599→            write_futures.push_back(std::async(std::launch::async, [&] {
  1600→                std::ofstream f(output_dir + "/interp_nodes.bin", std::ios::binary);
  1601→                f.write(reinterpret_cast<const char*>(data.interp_nodes.data()), data.interp_nodes.size() * sizeof(NodeCoord));
  1602→            }));
  1603→        }
  1604→    }
  1605→
  1606→    write_futures.push_back(std::async(std::launch::async, [&] {
  1607→        std::ofstream f(output_dir + "/admin_polygons.bin", std::ios::binary);
  1608→        f.write(reinterpret_cast<const char*>(data.admin_polygons.data()), data.admin_polygons.size() * sizeof(AdminPolygon));
  1609→    }));
  1610→
  1611→    write_futures.push_back(std::async(std::launch::async, [&] {
  1612→        std::ofstream f(output_dir + "/admin_vertices.bin", std::ios::binary);
  1613→        f.write(reinterpret_cast<const char*>(data.admin_vertices.data()), data.admin_vertices.size() * sizeof(NodeCoord));
  1614→    }));
  1615→
  1616→    write_futures.push_back(std::async(std::launch::async, [&] {
  1617→        std::ofstream f(output_dir + "/strings.bin", std::ios::binary);
  1618→        f.write(data.string_pool.data().data(), data.string_pool.data().size());
  1619→        std::cerr << "strings.bin: " << data.string_pool.data().size() << " bytes" << std::endl;
  1620→    }));
  1621→
  1622→    // Wait for all parallel writes
  1623→    log_phase("  Write: parallel data files launched", _wt);
  1624→    admin_future.get();
  1625→    for (auto& f : write_futures) f.get();
  1626→}
  1627→
  1628→// --- Main ---

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

    // Find where each chunk of sorted_cells starts in sorted_pairs
    // using binary search (both are sorted by cell_id)
    unsigned int nthreads = std::min(std::thread::hardware_concurrency(), 32u);
    if (nthreads == 0) nthreads = 4;
    size_t cells_per_chunk = (sorted_cells.size() + nthreads - 1) / nthreads;

    struct ChunkResult {
        std::vector<char> buf;
        size_t cell_start, cell_end; // range in sorted_cells
        uint32_t local_size; // total bytes in this chunk's buffer
    };
    std::vector<ChunkResult> chunks(nthreads);

    // Parallel: each thread processes its range of sorted_cells
    std::vector<std::thread> threads;
    for (unsigned int t = 0; t < nthreads; t++) {
        size_t cs = t * cells_per_chunk;
        size_t ce = std::min(cs + cells_per_chunk, sorted_cells.size());
        if (cs >= sorted_cells.size()) break;

        threads.emplace_back([&, t, cs, ce]() {
            auto& chunk = chunks[t];
            chunk.cell_start = cs;
            chunk.cell_end = ce;
            chunk.local_size = 0;

            // Binary search for where this chunk's first cell appears in sorted_pairs
            size_t pi = std::lower_bound(sorted_pairs.begin(), sorted_pairs.end(),
                sorted_cells[cs], [](const CellItemPair& p, uint64_t id) {
                    return p.cell_id < id;
                }) - sorted_pairs.begin();

            // Merge-join for this chunk
            for (size_t si = cs; si < ce && pi < sorted_pairs.size(); si++) {
                if (sorted_cells[si] < sorted_pairs[pi].cell_id) continue;
                while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id < sorted_cells[si]) pi++;
                if (pi >= sorted_pairs.size() || sorted_pairs[pi].cell_id != sorted_cells[si]) continue;

                offsets[si] = chunk.local_size; // local offset, adjusted later
                size_t start = pi;
                while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id == sorted_cells[si]) pi++;
                uint16_t count = static_cast<uint16_t>(std::min(pi - start, size_t(65535)));
                size_t entry_size = sizeof(uint16_t) + (pi - start) * sizeof(uint32_t);
                size_t buf_pos = chunk.buf.size();
                chunk.buf.resize(buf_pos + entry_size);
                memcpy(chunk.buf.data() + buf_pos, &count, sizeof(count));
                for (size_t k = start; k < pi; k++) {
                    memcpy(chunk.buf.data() + buf_pos + sizeof(uint16_t) + (k - start) * sizeof(uint32_t),
                           &sorted_pairs[k].item_id, sizeof(uint32_t));
                }
                chunk.local_size += entry_size;
            }
        });
    }
    for (auto& t : threads) t.join();

    // Prefix-sum: compute global offsets for each chunk
    uint32_t global_offset = 0;
    for (auto& chunk : chunks) {
        for (size_t si = chunk.cell_start; si < chunk.cell_end; si++) {
            if (offsets[si] != NO_DATA) {
                offsets[si] += global_offset;
            }
        }
        global_offset += chunk.local_size;
    }

    // Concatenate buffers and write
    std::vector<char> buf;
    buf.reserve(global_offset);
    for (auto& chunk : chunks) {
        buf.insert(buf.end(), chunk.buf.begin(), chunk.buf.end());
    }

    std::ofstream f(path, std::ios::binary);
    f.write(buf.data(), buf.size());
    return offsets;
}

static void write_cell_index(
    const std::string& cells_path,
    const std::string& entries_path,
    const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map
) {
    std::vector<std::pair<uint64_t, std::vector<uint32_t>>> sorted(
        cell_map.begin(), cell_map.end());
    std::sort(sorted.begin(), sorted.end());

    {
        std::ofstream f(cells_path, std::ios::binary);
        uint32_t current_offset = 0;
        for (const auto& [cell_id, ids] : sorted) {
            f.write(reinterpret_cast<const char*>(&cell_id), sizeof(cell_id));
            f.write(reinterpret_cast<const char*>(&current_offset), sizeof(current_offset));
            current_offset += sizeof(uint16_t) + ids.size() * sizeof(uint32_t);
        }
    }

    {
        std::ofstream f(entries_path, std::ios::binary);
        for (const auto& [cell_id, ids] : sorted) {
            uint16_t count = static_cast<uint16_t>(std::min(ids.size(), size_t(65535)));
            f.write(reinterpret_cast<const char*>(&count), sizeof(count));
            f.write(reinterpret_cast<const char*>(ids.data()), ids.size() * sizeof(uint32_t));
        }
    }
}

// --- Write all index files ---

static void write_index(const ParsedData& data, const std::string& output_dir, IndexMode mode) {
    ensure_dir(output_dir);
    auto _wt = std::chrono::steady_clock::now();

    bool write_streets = (mode != IndexMode::AdminOnly);
    bool write_addresses = (mode == IndexMode::Full);

    if (write_streets) {
        // Build sorted_geo_cells from sorted pairs where available (avoids hash map iteration).
        // Extract unique cell IDs from each sorted pair vector in parallel, then merge.
        std::vector<uint64_t> sorted_geo_cells;
        {
            auto extract_unique_cells = [](const std::vector<CellItemPair>& pairs) {
                std::vector<uint64_t> cells;
                cells.reserve(pairs.size() / 2);
                for (size_t i = 0; i < pairs.size(); ) {
                    cells.push_back(pairs[i].cell_id);
                    uint64_t cur = pairs[i].cell_id;
                    while (i < pairs.size() && pairs[i].cell_id == cur) i++;
                }
                return cells;
            };
            auto extract_from_map = [](const std::unordered_map<uint64_t, std::vector<uint32_t>>& m) {
                std::vector<uint64_t> cells;
                cells.reserve(m.size());
                for (auto& [id, _] : m) cells.push_back(id);
                std::sort(cells.begin(), cells.end());
                return cells;
            };

            // Extract unique cell IDs in parallel
            std::vector<uint64_t> way_cells, addr_cells, interp_cells;
            {
                auto f1 = std::async(std::launch::async, [&] {
                    return !data.sorted_way_cells.empty()
                        ? extract_unique_cells(data.sorted_way_cells)
                        : extract_from_map(data.cell_to_ways);
                });
                if (write_addresses) {
                    auto f2 = std::async(std::launch::async, [&] {
                        return !data.sorted_addr_cells.empty()
                            ? extract_unique_cells(data.sorted_addr_cells)
                            : extract_from_map(data.cell_to_addrs);
                    });
                    auto f3 = std::async(std::launch::async, [&] {
                        return extract_from_map(data.cell_to_interps);
                    });
                    addr_cells = f2.get();
                    interp_cells = f3.get();
                }
                way_cells = f1.get();
            }

            // Merge sorted unique cell ID vectors
            sorted_geo_cells.reserve(way_cells.size() + addr_cells.size() + interp_cells.size());
            std::merge(way_cells.begin(), way_cells.end(),
                       addr_cells.begin(), addr_cells.end(),
                       std::back_inserter(sorted_geo_cells));
            if (!interp_cells.empty()) {
                std::vector<uint64_t> tmp;
                tmp.reserve(sorted_geo_cells.size() + interp_cells.size());
                std::merge(sorted_geo_cells.begin(), sorted_geo_cells.end(),
                           interp_cells.begin(), interp_cells.end(),
                           std::back_inserter(tmp));
                sorted_geo_cells = std::move(tmp);
            }
            // Deduplicate (merge may have duplicates from overlapping cells)
            sorted_geo_cells.erase(std::unique(sorted_geo_cells.begin(), sorted_geo_cells.end()),
                                    sorted_geo_cells.end());
        }

        // Write entry files in parallel (returns positional offset vectors, not hash maps)
        std::vector<uint32_t> street_offsets, addr_offsets, interp_offsets;
        {
            auto f1 = std::async(std::launch::async, [&]() {
                // Use direct sorted-pair writing for ways (skips hash map extraction + sort)
                if (!data.sorted_way_cells.empty()) {
                    return write_entries_from_sorted(output_dir + "/street_entries.bin", sorted_geo_cells, data.sorted_way_cells);
                }
                return write_entries(output_dir + "/street_entries.bin", sorted_geo_cells, data.cell_to_ways);
            });
            if (write_addresses) {
                auto f2 = std::async(std::launch::async, [&]() {
                    if (!data.sorted_addr_cells.empty()) {
                        return write_entries_from_sorted(output_dir + "/addr_entries.bin", sorted_geo_cells, data.sorted_addr_cells);
                    }
                    return write_entries(output_dir + "/addr_entries.bin", sorted_geo_cells, data.cell_to_addrs);
                });
                auto f3 = std::async(std::launch::async, [&]() {
                    return write_entries(output_dir + "/interp_entries.bin", sorted_geo_cells, data.cell_to_interps);
                });
                addr_offsets = f2.get();
                interp_offsets = f3.get();
            }
            street_offsets = f1.get();
        }

        log_phase("  Write: entry files", _wt);
        // Build geo_cells.bin — parallel buffer fill + single write
        {
            size_t n = sorted_geo_cells.size();
            size_t row_size = sizeof(uint64_t) + 3 * sizeof(uint32_t);
            std::vector<char> buf(n * row_size);

            if (addr_offsets.empty()) addr_offsets.resize(n, NO_DATA);
            if (interp_offsets.empty()) interp_offsets.resize(n, NO_DATA);

            // Parallel buffer fill — split into chunks across threads
            unsigned int nthreads = std::thread::hardware_concurrency();
            if (nthreads == 0) nthreads = 4;
            size_t chunk = (n + nthreads - 1) / nthreads;
            std::vector<std::thread> fill_threads;
            for (unsigned int t = 0; t < nthreads; t++) {
                size_t start = t * chunk;
                size_t end = std::min(start + chunk, n);
                if (start >= n) break;
                fill_threads.emplace_back([&, start, end]() {
                    char* ptr = buf.data() + start * row_size;
                    for (size_t i = start; i < end; i++) {
                        memcpy(ptr, &sorted_geo_cells[i], sizeof(uint64_t)); ptr += sizeof(uint64_t);
                        memcpy(ptr, &street_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
                        memcpy(ptr, &addr_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
                        memcpy(ptr, &interp_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
                    }
                });
            }
            for (auto& t : fill_threads) t.join();

            std::ofstream f(output_dir + "/geo_cells.bin", std::ios::binary);
            f.write(buf.data(), buf.size());
        }

        std::cerr << "geo index: " << sorted_geo_cells.size() << " cells ("
                  << data.ways.size() << " ways, "
                  << data.addr_points.size() << " addrs, "
                  << data.interp_ways.size() << " interps)" << std::endl;
    }

    log_phase("  Write: geo_cells.bin", _wt);
    // Write admin cell index, raw data files, and admin polygons in parallel
    auto admin_future = std::async(std::launch::async, [&] {
        write_cell_index(output_dir + "/admin_cells.bin", output_dir + "/admin_entries.bin", data.cell_to_admin);
        std::cerr << "admin index: " << data.cell_to_admin.size() << " cells, " << data.admin_polygons.size() << " polygons" << std::endl;
    });

    std::vector<std::future<void>> write_futures;

    if (write_streets) {
        write_futures.push_back(std::async(std::launch::async, [&] {
            std::ofstream f(output_dir + "/street_ways.bin", std::ios::binary);
            f.write(reinterpret_cast<const char*>(data.ways.data()), data.ways.size() * sizeof(WayHeader));
        }));
        write_futures.push_back(std::async(std::launch::async, [&] {
            std::ofstream f(output_dir + "/street_nodes.bin", std::ios::binary);
            f.write(reinterpret_cast<const char*>(data.street_nodes.data()), data.street_nodes.size() * sizeof(NodeCoord));
        }));
        if (write_addresses) {
            write_futures.push_back(std::async(std::launch::async, [&] {
                std::ofstream f(output_dir + "/addr_points.bin", std::ios::binary);
                f.write(reinterpret_cast<const char*>(data.addr_points.data()), data.addr_points.size() * sizeof(AddrPoint));
            }));
            write_futures.push_back(std::async(std::launch::async, [&] {
                std::ofstream f(output_dir + "/interp_ways.bin", std::ios::binary);
                f.write(reinterpret_cast<const char*>(data.interp_ways.data()), data.interp_ways.size() * sizeof(InterpWay));
            }));
            write_futures.push_back(std::async(std::launch::async, [&] {
                std::ofstream f(output_dir + "/interp_nodes.bin", std::ios::binary);
                f.write(reinterpret_cast<const char*>(data.interp_nodes.data()), data.interp_nodes.size() * sizeof(NodeCoord));
            }));
        }
    }

    write_futures.push_back(std::async(std::launch::async, [&] {
        std::ofstream f(output_dir + "/admin_polygons.bin", std::ios::binary);
        f.write(reinterpret_cast<const char*>(data.admin_polygons.data()), data.admin_polygons.size() * sizeof(AdminPolygon));
    }));

    write_futures.push_back(std::async(std::launch::async, [&] {
        std::ofstream f(output_dir + "/admin_vertices.bin", std::ios::binary);
        f.write(reinterpret_cast<const char*>(data.admin_vertices.data()), data.admin_vertices.size() * sizeof(NodeCoord));
    }));

    write_futures.push_back(std::async(std::launch::async, [&] {
        std::ofstream f(output_dir + "/strings.bin", std::ios::binary);
        f.write(data.string_pool.data().data(), data.string_pool.data().size());
        std::cerr << "strings.bin: " << data.string_pool.data().size() << " bytes" << std::endl;
    }));

    // Wait for all parallel writes
    log_phase("  Write: parallel data files launched", _wt);
    admin_future.get();
    for (auto& f : write_futures) f.get();
}

// --- Main ---