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

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

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

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

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

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

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

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

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

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

// --- Cache serialization ---

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // Build remap: old offset -> new offset
    const auto& old_sp = full.string_pool.data();
    std::unordered_map<uint32_t, uint32_t> string_remap;
    auto& new_sp = out.string_pool.mutable_data();
    new_sp.clear();

    std::vector<uint32_t> sorted_offsets(used_offsets.begin(), used_offsets.end());
    std::sort(sorted_offsets.begin(), sorted_offsets.end());

    for (uint32_t old_off : sorted_offsets) {
        uint32_t new_off = static_cast<uint32_t>(new_sp.size());
        string_remap[old_off] = new_off;
        // Copy the null-terminated string
        const char* str = old_sp.data() + old_off;
        size_t len = std::strlen(str);
        new_sp.insert(new_sp.end(), str, str + len + 1);
    }

    // Remap all string IDs in the output data
    for (auto& w : out.ways) {
        w.name_id = string_remap[w.name_id];
    }
    for (auto& a : out.addr_points) {
        a.housenumber_id = string_remap[a.housenumber_id];
        a.street_id = string_remap[a.street_id];
    }
    for (auto& iw : out.interp_ways) {
        iw.street_id = string_remap[iw.street_id];
    }
    for (auto& ap : out.admin_polygons) {
        ap.name_id = string_remap[ap.name_id];
    }

    return out;
}

// --- Write cell index ---

static const uint32_t NO_DATA = 0xFFFFFFFFu;

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

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

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

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


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


  1600→    }
  1601→
  1602→    // Find where each chunk of sorted_cells starts in sorted_pairs
  1603→    // using binary search (both are sorted by cell_id)
  1604→    unsigned int nthreads = std::min(std::thread::hardware_concurrency(), 32u);
  1605→    if (nthreads == 0) nthreads = 4;
  1606→    size_t cells_per_chunk = (sorted_cells.size() + nthreads - 1) / nthreads;
  1607→
  1608→    struct ChunkResult {
  1609→        std::vector<char> buf;
  1610→        size_t cell_start, cell_end; // range in sorted_cells
  1611→        uint32_t local_size; // total bytes in this chunk's buffer
  1612→    };
  1613→    std::vector<ChunkResult> chunks(nthreads);
  1614→
  1615→    // Parallel: each thread processes its range of sorted_cells
  1616→    std::vector<std::thread> threads;
  1617→    for (unsigned int t = 0; t < nthreads; t++) {
  1618→        size_t cs = t * cells_per_chunk;
  1619→        size_t ce = std::min(cs + cells_per_chunk, sorted_cells.size());
  1620→        if (cs >= sorted_cells.size()) break;
  1621→
  1622→        threads.emplace_back([&, t, cs, ce]() {
  1623→            auto& chunk = chunks[t];
  1624→            chunk.cell_start = cs;
  1625→            chunk.cell_end = ce;
  1626→            chunk.local_size = 0;
  1627→
  1628→            // Binary search for where this chunk's first cell appears in sorted_pairs
  1629→            size_t pi = std::lower_bound(sorted_pairs.begin(), sorted_pairs.end(),
  1630→                sorted_cells[cs], [](const CellItemPair& p, uint64_t id) {
  1631→                    return p.cell_id < id;
  1632→                }) - sorted_pairs.begin();
  1633→
  1634→            // Merge-join for this chunk
  1635→            for (size_t si = cs; si < ce && pi < sorted_pairs.size(); si++) {
  1636→                if (sorted_cells[si] < sorted_pairs[pi].cell_id) continue;
  1637→                while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id < sorted_cells[si]) pi++;
  1638→                if (pi >= sorted_pairs.size() || sorted_pairs[pi].cell_id != sorted_cells[si]) continue;
  1639→
  1640→                offsets[si] = chunk.local_size; // local offset, adjusted later
  1641→                size_t start = pi;
  1642→                while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id == sorted_cells[si]) pi++;
  1643→                uint16_t count = static_cast<uint16_t>(std::min(pi - start, size_t(65535)));
  1644→                size_t entry_size = sizeof(uint16_t) + (pi - start) * sizeof(uint32_t);
  1645→                size_t buf_pos = chunk.buf.size();
  1646→                chunk.buf.resize(buf_pos + entry_size);
  1647→                memcpy(chunk.buf.data() + buf_pos, &count, sizeof(count));
  1648→                for (size_t k = start; k < pi; k++) {
  1649→                    memcpy(chunk.buf.data() + buf_pos + sizeof(uint16_t) + (k - start) * sizeof(uint32_t),
  1650→                           &sorted_pairs[k].item_id, sizeof(uint32_t));
  1651→                }
  1652→                chunk.local_size += entry_size;
  1653→            }
  1654→        });
  1655→    }
  1656→    for (auto& t : threads) t.join();
  1657→
  1658→    // Prefix-sum: compute global offsets for each chunk
  1659→    uint32_t global_offset = 0;
  1660→    for (auto& chunk : chunks) {
  1661→        for (size_t si = chunk.cell_start; si < chunk.cell_end; si++) {
  1662→            if (offsets[si] != NO_DATA) {
  1663→                offsets[si] += global_offset;
  1664→            }
  1665→        }
  1666→        global_offset += chunk.local_size;
  1667→    }
  1668→
  1669→    // Concatenate buffers and write
  1670→    std::vector<char> buf;
  1671→    buf.reserve(global_offset);
  1672→    for (auto& chunk : chunks) {
  1673→        buf.insert(buf.end(), chunk.buf.begin(), chunk.buf.end());
  1674→    }
  1675→
  1676→    std::ofstream f(path, std::ios::binary);
  1677→    f.write(buf.data(), buf.size());
  1678→    return offsets;
  1679→}
  1680→
  1681→static void write_cell_index(
  1682→    const std::string& cells_path,
  1683→    const std::string& entries_path,
  1684→    const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map
  1685→) {
  1686→    std::vector<std::pair<uint64_t, std::vector<uint32_t>>> sorted(
  1687→        cell_map.begin(), cell_map.end());
  1688→    std::sort(sorted.begin(), sorted.end());
  1689→
  1690→    {
  1691→        std::ofstream f(cells_path, std::ios::binary);
  1692→        uint32_t current_offset = 0;
  1693→        for (const auto& [cell_id, ids] : sorted) {
  1694→            f.write(reinterpret_cast<const char*>(&cell_id), sizeof(cell_id));
  1695→            f.write(reinterpret_cast<const char*>(&current_offset), sizeof(current_offset));
  1696→            current_offset += sizeof(uint16_t) + ids.size() * sizeof(uint32_t);
  1697→        }
  1698→    }
  1699→
  1700→    {
  1701→        std::ofstream f(entries_path, std::ios::binary);
  1702→        for (const auto& [cell_id, ids] : sorted) {
  1703→            uint16_t count = static_cast<uint16_t>(std::min(ids.size(), size_t(65535)));
  1704→            f.write(reinterpret_cast<const char*>(&count), sizeof(count));
  1705→            f.write(reinterpret_cast<const char*>(ids.data()), ids.size() * sizeof(uint32_t));
  1706→        }
  1707→    }
  1708→}
  1709→
  1710→// --- Write all index files ---
  1711→
  1712→static void write_index(const ParsedData& data, const std::string& output_dir, IndexMode mode) {
  1713→    ensure_dir(output_dir);
  1714→    auto _wt = std::chrono::steady_clock::now();
  1715→
  1716→    bool write_streets = (mode != IndexMode::AdminOnly);
  1717→    bool write_addresses = (mode == IndexMode::Full);
  1718→
  1719→    if (write_streets) {
  1720→        // Build sorted_geo_cells from sorted pairs where available (avoids hash map iteration).
  1721→        // Extract unique cell IDs from each sorted pair vector in parallel, then merge.
  1722→        std::vector<uint64_t> sorted_geo_cells;
  1723→        {
  1724→            auto extract_unique_cells = [](const std::vector<CellItemPair>& pairs) {
  1725→                std::vector<uint64_t> cells;
  1726→                cells.reserve(pairs.size() / 2);
  1727→                for (size_t i = 0; i < pairs.size(); ) {
  1728→                    cells.push_back(pairs[i].cell_id);
  1729→                    uint64_t cur = pairs[i].cell_id;
  1730→                    while (i < pairs.size() && pairs[i].cell_id == cur) i++;
  1731→                }
  1732→                return cells;
  1733→            };
  1734→            auto extract_from_map = [](const std::unordered_map<uint64_t, std::vector<uint32_t>>& m) {
  1735→                std::vector<uint64_t> cells;
  1736→                cells.reserve(m.size());
  1737→                for (auto& [id, _] : m) cells.push_back(id);
  1738→                std::sort(cells.begin(), cells.end());
  1739→                return cells;
  1740→            };
  1741→
  1742→            // Extract unique cell IDs in parallel
  1743→            std::vector<uint64_t> way_cells, addr_cells, interp_cells;
  1744→            {
  1745→                auto f1 = std::async(std::launch::async, [&] {
  1746→                    return !data.sorted_way_cells.empty()
  1747→                        ? extract_unique_cells(data.sorted_way_cells)
  1748→                        : extract_from_map(data.cell_to_ways);
  1749→                });
  1750→                if (write_addresses) {
  1751→                    auto f2 = std::async(std::launch::async, [&] {
  1752→                        return !data.sorted_addr_cells.empty()
  1753→                            ? extract_unique_cells(data.sorted_addr_cells)
  1754→                            : extract_from_map(data.cell_to_addrs);
  1755→                    });
  1756→                    auto f3 = std::async(std::launch::async, [&] {
  1757→                        return extract_from_map(data.cell_to_interps);
  1758→                    });
  1759→                    addr_cells = f2.get();
  1760→                    interp_cells = f3.get();
  1761→                }
  1762→                way_cells = f1.get();
  1763→            }
  1764→
  1765→            // Merge sorted unique cell ID vectors
  1766→            sorted_geo_cells.reserve(way_cells.size() + addr_cells.size() + interp_cells.size());
  1767→            std::merge(way_cells.begin(), way_cells.end(),
  1768→                       addr_cells.begin(), addr_cells.end(),
  1769→                       std::back_inserter(sorted_geo_cells));
  1770→            if (!interp_cells.empty()) {
  1771→                std::vector<uint64_t> tmp;
  1772→                tmp.reserve(sorted_geo_cells.size() + interp_cells.size());
  1773→                std::merge(sorted_geo_cells.begin(), sorted_geo_cells.end(),
  1774→                           interp_cells.begin(), interp_cells.end(),
  1775→                           std::back_inserter(tmp));
  1776→                sorted_geo_cells = std::move(tmp);
  1777→            }
  1778→            // Deduplicate (merge may have duplicates from overlapping cells)
  1779→            sorted_geo_cells.erase(std::unique(sorted_geo_cells.begin(), sorted_geo_cells.end()),
  1780→                                    sorted_geo_cells.end());
  1781→        }
  1782→
  1783→        // Write entry files in parallel (returns positional offset vectors, not hash maps)
  1784→        std::vector<uint32_t> street_offsets, addr_offsets, interp_offsets;
  1785→        {
  1786→            auto f1 = std::async(std::launch::async, [&]() {
  1787→                // Use direct sorted-pair writing for ways (skips hash map extraction + sort)
  1788→                if (!data.sorted_way_cells.empty()) {
  1789→                    return write_entries_from_sorted(output_dir + "/street_entries.bin", sorted_geo_cells, data.sorted_way_cells);
  1790→                }
  1791→                return write_entries(output_dir + "/street_entries.bin", sorted_geo_cells, data.cell_to_ways);
  1792→            });
  1793→            if (write_addresses) {
  1794→                auto f2 = std::async(std::launch::async, [&]() {
  1795→                    if (!data.sorted_addr_cells.empty()) {
  1796→                        return write_entries_from_sorted(output_dir + "/addr_entries.bin", sorted_geo_cells, data.sorted_addr_cells);
  1797→                    }
  1798→                    return write_entries(output_dir + "/addr_entries.bin", sorted_geo_cells, data.cell_to_addrs);
  1799→                });
  1800→                auto f3 = std::async(std::launch::async, [&]() {
  1801→                    return write_entries(output_dir + "/interp_entries.bin", sorted_geo_cells, data.cell_to_interps);
  1802→                });
  1803→                addr_offsets = f2.get();
  1804→                interp_offsets = f3.get();
  1805→            }
  1806→            street_offsets = f1.get();
  1807→        }
  1808→
  1809→        log_phase("  Write: entry files", _wt);
  1810→        // Build geo_cells.bin — parallel buffer fill + single write
  1811→        {
  1812→            size_t n = sorted_geo_cells.size();
  1813→            size_t row_size = sizeof(uint64_t) + 3 * sizeof(uint32_t);
  1814→            std::vector<char> buf(n * row_size);
  1815→
  1816→            if (addr_offsets.empty()) addr_offsets.resize(n, NO_DATA);
  1817→            if (interp_offsets.empty()) interp_offsets.resize(n, NO_DATA);
  1818→
  1819→            // Parallel buffer fill — split into chunks across threads
  1820→            unsigned int nthreads = std::thread::hardware_concurrency();
  1821→            if (nthreads == 0) nthreads = 4;
  1822→            size_t chunk = (n + nthreads - 1) / nthreads;
  1823→            std::vector<std::thread> fill_threads;
  1824→            for (unsigned int t = 0; t < nthreads; t++) {
  1825→                size_t start = t * chunk;
  1826→                size_t end = std::min(start + chunk, n);
  1827→                if (start >= n) break;
  1828→                fill_threads.emplace_back([&, start, end]() {
  1829→                    char* ptr = buf.data() + start * row_size;
  1830→                    for (size_t i = start; i < end; i++) {
  1831→                        memcpy(ptr, &sorted_geo_cells[i], sizeof(uint64_t)); ptr += sizeof(uint64_t);
  1832→                        memcpy(ptr, &street_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
  1833→                        memcpy(ptr, &addr_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
  1834→                        memcpy(ptr, &interp_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
  1835→                    }
  1836→                });
  1837→            }
  1838→            for (auto& t : fill_threads) t.join();
  1839→
  1840→            std::ofstream f(output_dir + "/geo_cells.bin", std::ios::binary);
  1841→            f.write(buf.data(), buf.size());
  1842→        }
  1843→
  1844→        std::cerr << "geo index: " << sorted_geo_cells.size() << " cells ("
  1845→                  << data.ways.size() << " ways, "
  1846→                  << data.addr_points.size() << " addrs, "
  1847→                  << data.interp_ways.size() << " interps)" << std::endl;
  1848→    }
  1849→
  1850→    log_phase("  Write: geo_cells.bin", _wt);
  1851→    // Write admin cell index, raw data files, and admin polygons in parallel
  1852→    auto admin_future = std::async(std::launch::async, [&] {
  1853→        write_cell_index(output_dir + "/admin_cells.bin", output_dir + "/admin_entries.bin", data.cell_to_admin);
  1854→        std::cerr << "admin index: " << data.cell_to_admin.size() << " cells, " << data.admin_polygons.size() << " polygons" << std::endl;
  1855→    });
  1856→
  1857→    std::vector<std::future<void>> write_futures;
  1858→
  1859→    if (write_streets) {
  1860→        write_futures.push_back(std::async(std::launch::async, [&] {
  1861→            std::ofstream f(output_dir + "/street_ways.bin", std::ios::binary);
  1862→            f.write(reinterpret_cast<const char*>(data.ways.data()), data.ways.size() * sizeof(WayHeader));
  1863→        }));
  1864→        write_futures.push_back(std::async(std::launch::async, [&] {
  1865→            std::ofstream f(output_dir + "/street_nodes.bin", std::ios::binary);
  1866→            f.write(reinterpret_cast<const char*>(data.street_nodes.data()), data.street_nodes.size() * sizeof(NodeCoord));
  1867→        }));
  1868→        if (write_addresses) {
  1869→            write_futures.push_back(std::async(std::launch::async, [&] {
  1870→                std::ofstream f(output_dir + "/addr_points.bin", std::ios::binary);
  1871→                f.write(reinterpret_cast<const char*>(data.addr_points.data()), data.addr_points.size() * sizeof(AddrPoint));
  1872→            }));
  1873→            write_futures.push_back(std::async(std::launch::async, [&] {
  1874→                std::ofstream f(output_dir + "/interp_ways.bin", std::ios::binary);
  1875→                f.write(reinterpret_cast<const char*>(data.interp_ways.data()), data.interp_ways.size() * sizeof(InterpWay));
  1876→            }));
  1877→            write_futures.push_back(std::async(std::launch::async, [&] {
  1878→                std::ofstream f(output_dir + "/interp_nodes.bin", std::ios::binary);
  1879→                f.write(reinterpret_cast<const char*>(data.interp_nodes.data()), data.interp_nodes.size() * sizeof(NodeCoord));
  1880→            }));
  1881→        }
  1882→    }
  1883→
  1884→    write_futures.push_back(std::async(std::launch::async, [&] {
  1885→        std::ofstream f(output_dir + "/admin_polygons.bin", std::ios::binary);
  1886→        f.write(reinterpret_cast<const char*>(data.admin_polygons.data()), data.admin_polygons.size() * sizeof(AdminPolygon));
  1887→    }));
  1888→
  1889→    write_futures.push_back(std::async(std::launch::async, [&] {
  1890→        std::ofstream f(output_dir + "/admin_vertices.bin", std::ios::binary);
  1891→        f.write(reinterpret_cast<const char*>(data.admin_vertices.data()), data.admin_vertices.size() * sizeof(NodeCoord));
  1892→    }));
  1893→
  1894→    write_futures.push_back(std::async(std::launch::async, [&] {
  1895→        std::ofstream f(output_dir + "/strings.bin", std::ios::binary);
  1896→        f.write(data.string_pool.data().data(), data.string_pool.data().size());
  1897→        std::cerr << "strings.bin: " << data.string_pool.data().size() << " bytes" << std::endl;
  1898→    }));
  1899→
  1900→    // Wait for all parallel writes
  1901→    log_phase("  Write: parallel data files launched", _wt);
  1902→    admin_future.get();
  1903→    for (auto& f : write_futures) f.get();
  1904→}
  1905→
  1906→// --- Main ---
  1907→
  1908→int main(int argc, char* argv[]) {
  1909→    if (argc < 2) {
  1910→        std::cerr << "Usage: build-index <output-dir> <input.osm.pbf> [options]" << std::endl;
  1911→        std::cerr << "       build-index <output-dir> --load-cache <path> [options]" << std::endl;
  1912→        std::cerr << std::endl;
  1913→        std::cerr << "Options:" << std::endl;
  1914→        std::cerr << "  --street-level N       S2 cell level for streets (default: 17)" << std::endl;
  1915→        std::cerr << "  --admin-level N        S2 cell level for admin (default: 10)" << std::endl;
  1916→        std::cerr << "  --max-admin-level N    Only include admin levels <= N" << std::endl;
  1917→        std::cerr << "  --save-cache <path>    Save parsed data to cache file" << std::endl;
  1918→        std::cerr << "  --load-cache <path>    Load from cache instead of PBF" << std::endl;
  1919→        std::cerr << "  --multi-output         Write full, no-addresses, admin-only indexes" << std::endl;
  1920→        std::cerr << "  --continents           Also generate per-continent indexes" << std::endl;
  1921→        std::cerr << "  --mode <mode>          Index mode: full, no-addresses, admin-only (default: full)" << std::endl;
  1922→        std::cerr << "  --admin-only           Shorthand for --mode admin-only" << std::endl;
  1923→        std::cerr << "  --no-addresses         Shorthand for --mode no-addresses" << std::endl;
  1924→        return 1;
  1925→    }
  1926→
  1927→    std::string output_dir = argv[1];
  1928→    std::vector<std::string> input_files;
  1929→    std::string save_cache_path;
  1930→    std::string load_cache_path;
  1931→    bool multi_output = false;
  1932→    bool generate_continents = false;
  1933→    IndexMode mode = IndexMode::Full;
  1934→
  1935→    for (int i = 2; i < argc; i++) {
  1936→        std::string arg = argv[i];
  1937→        if (arg == "--street-level" && i + 1 < argc) {
  1938→            kStreetCellLevel = std::atoi(argv[++i]);
  1939→        } else if (arg == "--admin-level" && i + 1 < argc) {
  1940→            kAdminCellLevel = std::atoi(argv[++i]);
  1941→        } else if (arg == "--admin-only") {
  1942→            mode = IndexMode::AdminOnly;
  1943→        } else if (arg == "--no-addresses") {
  1944→            mode = IndexMode::NoAddresses;
  1945→        } else if (arg == "--max-admin-level" && i + 1 < argc) {
  1946→            kMaxAdminLevel = std::atoi(argv[++i]);
  1947→        } else if (arg == "--save-cache" && i + 1 < argc) {
  1948→            save_cache_path = argv[++i];
  1949→        } else if (arg == "--load-cache" && i + 1 < argc) {
  1950→            load_cache_path = argv[++i];
  1951→        } else if (arg == "--multi-output") {
  1952→            multi_output = true;
  1953→        } else if (arg == "--continents") {
  1954→            generate_continents = true;
  1955→        } else if (arg == "--mode" && i + 1 < argc) {
  1956→            std::string mode_str = argv[++i];
  1957→            if (mode_str == "full") {
  1958→                mode = IndexMode::Full;
  1959→            } else if (mode_str == "no-addresses") {
  1960→                mode = IndexMode::NoAddresses;
  1961→            } else if (mode_str == "admin-only") {
  1962→                mode = IndexMode::AdminOnly;
  1963→            } else {
  1964→                std::cerr << "Error: unknown mode '" << mode_str << "'" << std::endl;
  1965→                return 1;
  1966→            }
  1967→        } else {
  1968→            input_files.push_back(arg);
  1969→        }
  1970→    }
  1971→
  1972→    ParsedData data;
  1973→    auto _pt = std::chrono::steady_clock::now();
  1974→
  1975→    if (!load_cache_path.empty()) {
  1976→        // Load from cache
  1977→        if (!deserialize_cache(data, load_cache_path)) {
  1978→            std::cerr << "Error: failed to load cache" << std::endl;
  1979→            return 1;
  1980→        }
  1981→    } else {
  1982→        // Parse PBF files (always collect everything)
  1983→        if (input_files.empty()) {
  1984→            std::cerr << "Error: no input files specified and no --load-cache" << std::endl;
  1985→            return 1;
  1986→        }
  1987→
  1988→        // Create thread pool for concurrent admin polygon S2 covering
  1989→        unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency() > 4 ? std::thread::hardware_concurrency() - 4 : 1u);
  1990→        std::cerr << "Using " << num_threads << " worker threads." << std::endl;
  1991→        AdminCoverPool admin_pool(num_threads);
  1992→        // BuildHandler no longer used — parallel processing handles everything
  1993→
  1994→        // Dense array node location index — lockless parallel writes
  1995→        // Planet OSM node IDs max ~12.5 billion. 8 bytes per Location = 100GB virtual.
  1996→        // MAP_NORESERVE means OS only allocates pages on write (~80GB for 10B nodes).
  1997→        static const size_t MAX_NODE_ID = 15000000000ULL;
  1998→        struct DenseIndex {
  1999→            osmium::Location* data;

    }

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

int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cerr << "Usage: build-index <output-dir> <input.osm.pbf> [options]" << std::endl;
        std::cerr << "       build-index <output-dir> --load-cache <path> [options]" << std::endl;
        std::cerr << std::endl;
        std::cerr << "Options:" << std::endl;
        std::cerr << "  --street-level N       S2 cell level for streets (default: 17)" << std::endl;
        std::cerr << "  --admin-level N        S2 cell level for admin (default: 10)" << std::endl;
        std::cerr << "  --max-admin-level N    Only include admin levels <= N" << std::endl;
        std::cerr << "  --save-cache <path>    Save parsed data to cache file" << std::endl;
        std::cerr << "  --load-cache <path>    Load from cache instead of PBF" << std::endl;
        std::cerr << "  --multi-output         Write full, no-addresses, admin-only indexes" << std::endl;
        std::cerr << "  --continents           Also generate per-continent indexes" << std::endl;
        std::cerr << "  --mode <mode>          Index mode: full, no-addresses, admin-only (default: full)" << std::endl;
        std::cerr << "  --admin-only           Shorthand for --mode admin-only" << std::endl;
        std::cerr << "  --no-addresses         Shorthand for --mode no-addresses" << std::endl;
        return 1;
    }

    std::string output_dir = argv[1];
    std::vector<std::string> input_files;
    std::string save_cache_path;
    std::string load_cache_path;
    bool multi_output = false;
    bool generate_continents = false;
    IndexMode mode = IndexMode::Full;

    for (int i = 2; i < argc; i++) {
        std::string arg = argv[i];
        if (arg == "--street-level" && i + 1 < argc) {
            kStreetCellLevel = std::atoi(argv[++i]);
        } else if (arg == "--admin-level" && i + 1 < argc) {
            kAdminCellLevel = std::atoi(argv[++i]);
        } else if (arg == "--admin-only") {
            mode = IndexMode::AdminOnly;
        } else if (arg == "--no-addresses") {
            mode = IndexMode::NoAddresses;
        } else if (arg == "--max-admin-level" && i + 1 < argc) {
            kMaxAdminLevel = std::atoi(argv[++i]);
        } else if (arg == "--save-cache" && i + 1 < argc) {
            save_cache_path = argv[++i];
        } else if (arg == "--load-cache" && i + 1 < argc) {
            load_cache_path = argv[++i];
        } else if (arg == "--multi-output") {
            multi_output = true;
        } else if (arg == "--continents") {
            generate_continents = true;
        } else if (arg == "--mode" && i + 1 < argc) {
            std::string mode_str = argv[++i];
            if (mode_str == "full") {
                mode = IndexMode::Full;
            } else if (mode_str == "no-addresses") {
                mode = IndexMode::NoAddresses;
            } else if (mode_str == "admin-only") {
                mode = IndexMode::AdminOnly;
            } else {
                std::cerr << "Error: unknown mode '" << mode_str << "'" << std::endl;
                return 1;
            }
        } else {
            input_files.push_back(arg);
        }
    }

    ParsedData data;
    auto _pt = std::chrono::steady_clock::now();

    if (!load_cache_path.empty()) {
        // Load from cache
        if (!deserialize_cache(data, load_cache_path)) {
            std::cerr << "Error: failed to load cache" << std::endl;
            return 1;
        }
    } else {
        // Parse PBF files (always collect everything)
        if (input_files.empty()) {
            std::cerr << "Error: no input files specified and no --load-cache" << std::endl;
            return 1;
        }

        // Create thread pool for concurrent admin polygon S2 covering
        unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency() > 4 ? std::thread::hardware_concurrency() - 4 : 1u);
        std::cerr << "Using " << num_threads << " worker threads." << std::endl;
        AdminCoverPool admin_pool(num_threads);
        // BuildHandler no longer used — parallel processing handles everything

        // Dense array node location index — lockless parallel writes
        // Planet OSM node IDs max ~12.5 billion. 8 bytes per Location = 100GB virtual.
        // MAP_NORESERVE means OS only allocates pages on write (~80GB for 10B nodes).
        static const size_t MAX_NODE_ID = 15000000000ULL;
        struct DenseIndex {
            osmium::Location* data;