Warning: Permanently added '100.113.88.121' (ED25519) to the list of known hosts.
=== Build A ===
strings.bin: 82546573 bytes
admin index: 141000 cells, 450907 polygons
  [2.4s 5.7/32cores 17% 16122MiB] All index writing (total)
Wrote /home/michtest/det-A/manifest.json
Done.
=== Build B ===
strings.bin: 82546573 bytes
admin index: 141000 cells, 450907 polygons
  [2.3s 6.2/32cores 19% 16098MiB] All index writing (total)
Wrote /home/michtest/det-B/manifest.json
Done.
=== Comparing ===
  addr_entries.bin: MATCH
  addr_points.bin: MATCH
  admin_cells.bin: MATCH
  admin_entries.bin: MATCH
  admin_polygons.bin: MATCH
  admin_vertices.bin: MATCH
  geo_cells.bin: MATCH
  interp_entries.bin: MATCH
  interp_nodes.bin: MATCH
  interp_ways.bin: MATCH
  street_entries.bin: MATCH
  street_nodes.bin: MATCH
  street_ways.bin: MATCH
  strings.bin: MATCH
  manifest.json: MATCH
ALL FILES MATCH


**All 14 files + manifest match — builds are deterministic!** Same PBF, two independent runs, byte-identical output.

Step 1 complete. Now let me measure what zstd --patch-from gives between two different deterministic builds (old vs new Europe PBFs):


Bash

sshpass -p 'elon-is-insane' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ProxyCommand="tailscale nc %h %p" michtest@100.113.88.121 '
# Build from new PBF with deterministic builder
rm -rf ~/det-new
~/build-index-det ~/det-new ~/pbf/europe-new.osm.pbf --simplify-epsilon --epsilon-scale 1 2>&1 | tail -5
echo "=== PATCHES: det-A (old PBF) vs det-new (new PBF) ==="
mkdir -p ~/patches-det
total=0
for f in ~/det-new/*.bin; do
    b=$(basename $f)
    old=~/det-A/$b
    if [ -f "$old" ]; then
        zstd --patch-from="$old" "$f" -o ~/patches-det/${b}.patch -f 2>/dev/null
        patchsize=$(stat -c%s ~/patches-det/${b}.patch 2>/dev/null || echo 0)
        newsize=$(stat -c%s $f)
        ratio=$(echo "scale=2; $patchsize*100/$newsize" | bc 2>/dev/null || echo "?")
        total=$((total + patchsize))
        printf "  %-25s %10s -> %10s (%s%%)\n" "$b" "$(numfmt --to=iec $newsize)" "$(numfmt --to=iec $patchsize)" "$ratio"
    fi
done
echo "  TOTAL: $(numfmt --to=iec $total)"
' 2>&1

Build new PBF deterministically, measure zstd patches


Warning: Permanently added '100.113.88.121' (ED25519) to the list of known hosts.
strings.bin: 82573397 bytes
admin index: 141000 cells, 450864 polygons
  [3.2s 4.5/32cores 14% 16405MiB] All index writing (total)
Wrote /home/michtest/det-new/manifest.json
Done.
=== PATCHES: det-A (old PBF) vs det-new (new PBF) ===
  addr_entries.bin                377M ->       299M (79.22%)
  addr_points.bin                 1.3G ->       711M (54.13%)
  admin_cells.bin                 1.7M ->       459K (27.72%)
  admin_entries.bin               5.9M ->       2.4M (40.60%)
  admin_polygons.bin               11M ->       4.8M (46.12%)
  admin_vertices.bin              278M ->       524K (.18%)
  geo_cells.bin                   1.6G ->       379M (23.67%)
  interp_entries.bin              156K ->        59K (37.89%)
  interp_nodes.bin                709K ->       1.2K (.16%)
  interp_ways.bin                 328K ->        74K (22.32%)
  street_entries.bin              560M ->       172M (30.60%)
  street_nodes.bin                1.4G ->       6.5M (.46%)
  street_ways.bin                 223M ->        69M (30.69%)
  strings.bin                      79M ->        70K (.08%)
  TOTAL: 1.7G


1.7 GiB — deterministic ordering alone doesn't help patch sizes because string offsets still differ between builds. But now we have the foundation for the real solution: **custom diff/patch with string remapping**.

The key: since builds are deterministic, `patch(det_build_1, patch_1→2)` produces exactly `det_build_2`. Then `patch_2→3` applies to exactly `det_build_2`. Sequential patching works.

Let me check if libzstd is available for the delta encoding:


apt-get install -y libzstd-dev 2>&1 | tail -3

Install libzstd development headers


Preparing to unpack .../libzstd-dev_1.5.7+dfsg-1_amd64.deb ...
Unpacking libzstd-dev:amd64 (1.5.7+dfsg-1) ...
Setting up libzstd-dev:amd64 (1.5.7+dfsg-1) ...


default-src 'none'; frame-ancestors 'none'

_cfuvid=Kqy.rfjF7JC9lff6CwE1Cvbmi9BpqBOj2JeRfV_6AiQ-1774607543.0392928-1.0.1.1-LDvAhXLWA0_oncwUnZ2SP3nXwSOlEJOFWVHVwi1mHBY; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com

Overloaded. https://docs.claude.com/en/api/errors


Good. Now let me rewrite the diff and patch tools properly with the string-remap + zstd delta approach:


Write

// geocoder-diff: Compare two deterministic geocoder builds, produce a .gcpatch.
//
// Strategy:
// 1. Compute string remap (old string pool → new string pool)
// 2. For each data file: apply remap to old file, zstd-delta against new file
// 3. For files without string refs: zstd-delta directly
// 4. Package into .gcpatch
//
// Because builds are deterministic, applying this patch to the old build
// produces output byte-identical to the new build. Sequential patching works.
//
// Usage: geocoder-diff <old-dir> <new-dir> -o <patch-file>

#include <algorithm>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <functional>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

#include <zstd.h>
#include "patch_format.h"

// --- String remap ---

struct StringRemapTable {
    // Flat array indexed by old byte offset → new byte offset
    // Size = old pool size. Only populated at string start positions.
    std::vector<uint32_t> table;
    size_t old_pool_size = 0;

    void build(const std::vector<char>& old_pool, const std::vector<char>& new_pool) {
        old_pool_size = old_pool.size();
        // Index new pool: string → offset
        std::unordered_map<std::string, uint32_t> new_idx;
        size_t pos = 0;
        while (pos < new_pool.size()) {
            const char* s = new_pool.data() + pos;
            size_t len = strlen(s);
            new_idx[std::string(s, len)] = static_cast<uint32_t>(pos);
            pos += len + 1;
        }
        // Build remap
        table.resize(old_pool.size(), 0);
        pos = 0;
        while (pos < old_pool.size()) {
            const char* s = old_pool.data() + pos;
            size_t len = strlen(s);
            auto it = new_idx.find(std::string(s, len));
            table[pos] = (it != new_idx.end()) ? it->second : 0;
            pos += len + 1;
        }
    }

    uint32_t remap(uint32_t old_off) const {
        return (old_off < table.size()) ? table[old_off] : 0;
    }
};

// Apply string remap to specific fields in data files
static void apply_remap_addr_points(std::vector<char>& data, const StringRemapTable& rm) {
    for (size_t i = 0; i + 16 <= data.size(); i += 16) {
        for (size_t off : {8, 12}) {
            uint32_t v; memcpy(&v, data.data() + i + off, 4);
            v = rm.remap(v);
            memcpy(data.data() + i + off, &v, 4);
        }
    }
}

static void apply_remap_ways(std::vector<char>& data, size_t stride, const StringRemapTable& rm) {
    size_t name_off = (stride == 12) ? 8 : 5;
    for (size_t i = 0; i + stride <= data.size(); i += stride) {
        uint32_t v; memcpy(&v, data.data() + i + name_off, 4);
        v = rm.remap(v);
        memcpy(data.data() + i + name_off, &v, 4);
    }
}

static void apply_remap_interps(std::vector<char>& data, size_t stride, const StringRemapTable& rm) {
    size_t street_off = (stride >= 20) ? 8 : 5;
    for (size_t i = 0; i + stride <= data.size(); i += stride) {
        uint32_t v; memcpy(&v, data.data() + i + street_off, 4);
        v = rm.remap(v);
        memcpy(data.data() + i + street_off, &v, 4);
    }
}

static void apply_remap_admin(std::vector<char>& data, size_t stride, const StringRemapTable& rm) {
    for (size_t i = 0; i + stride <= data.size(); i += stride) {
        uint32_t v; memcpy(&v, data.data() + i + 8, 4); // name_id at offset 8
        v = rm.remap(v);
        memcpy(data.data() + i + 8, &v, 4);
    }
}

// --- zstd delta compression ---
// Uses zstd's dictionary mode: old file as dictionary, compress new file
// This produces very compact deltas when files are similar.

static std::vector<char> zstd_delta_compress(const char* old_data, size_t old_size,
                                               const char* new_data, size_t new_size) {
    // Create a compression context with the old file as a "prefix"
    ZSTD_CCtx* cctx = ZSTD_createCCtx();
    ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, 19);
    ZSTD_CCtx_setParameter(cctx, ZSTD_c_windowLog, 31); // 2 GiB window
    ZSTD_CCtx_setParameter(cctx, ZSTD_c_enableLongDistanceMatching, ZSTD_ps_enable);

    // Reference the old file as prefix (zstd matches against it)
    ZSTD_CCtx_refPrefix(cctx, old_data, old_size);

    size_t bound = ZSTD_compressBound(new_size);
    std::vector<char> out(bound);
    size_t result = ZSTD_compress2(cctx, out.data(), bound, new_data, new_size);
    ZSTD_freeCCtx(cctx);

    if (ZSTD_isError(result)) {
        std::cerr << "zstd compress error: " << ZSTD_getErrorName(result) << std::endl;
        return {};
    }
    out.resize(result);
    return out;
}

// --- Write helpers ---
static void write_bytes(std::ofstream& f, const void* data, size_t size) {
    f.write(reinterpret_cast<const char*>(data), size);
}

int main(int argc, char* argv[]) {
    if (argc < 5 || std::string(argv[3]) != "-o") {
        std::cerr << "Usage: geocoder-diff <old-dir> <new-dir> -o <patch-file>" << std::endl;
        return 1;
    }
    std::string old_dir = argv[1], new_dir = argv[2], patch_path = argv[4];

    // Load string pools, build remap
    std::cerr << "Building string remap..." << std::endl;
    auto old_strings = read_file(old_dir + "/strings.bin");
    auto new_strings = read_file(new_dir + "/strings.bin");
    StringRemapTable remap;
    remap.build(old_strings, new_strings);

    // Detect struct strides from old files
    auto old_ways_raw = read_file(old_dir + "/street_ways.bin");
    size_t way_stride = (old_ways_raw.size() % 12 == 0 && old_ways_raw.size() > 0) ? 12 : 9;
    auto old_interps_raw = read_file(old_dir + "/interp_ways.bin");
    size_t interp_stride = (old_interps_raw.size() % 20 == 0 && old_interps_raw.size() > 0) ? 20 : 18;
    auto old_admin_raw = read_file(old_dir + "/admin_polygons.bin");
    size_t admin_stride = 24;
    for (size_t s : {24, 20, 19}) {
        if (old_admin_raw.size() % s == 0 && old_admin_raw.size() > 0) { admin_stride = s; break; }
    }
    // Free raw data (will reload per-file)
    old_ways_raw.clear(); old_interps_raw.clear(); old_admin_raw.clear();

    // Open patch file
    std::ofstream pf(patch_path, std::ios::binary);
    pf.write(GCPATCH_MAGIC, 8);
    uint32_t ver = GCPATCH_VERSION, flags = 0;
    write_bytes(pf, &ver, 4);
    write_bytes(pf, &flags, 4);

    // Section: string remap table + new strings
    {
        std::cerr << "  Writing string remap..." << std::endl;
        uint32_t section_id = 0xFFFFFFFE; // remap marker
        write_bytes(pf, &section_id, 4);
        uint64_t old_pool_size = remap.old_pool_size;
        write_bytes(pf, &old_pool_size, 8);
        // Write the full remap table (compact: only entries at string start positions)
        // Format: count, then (old_off, new_off) pairs
        std::vector<std::pair<uint32_t, uint32_t>> entries;
        size_t pos = 0;
        while (pos < old_strings.size()) {
            entries.push_back({static_cast<uint32_t>(pos), remap.table[pos]});
            pos += strlen(old_strings.data() + pos) + 1;
        }
        uint32_t count = static_cast<uint32_t>(entries.size());
        write_bytes(pf, &count, 4);
        for (auto& [o, n] : entries) { write_bytes(pf, &o, 4); write_bytes(pf, &n, 4); }
        std::cerr << "    " << count << " remap entries" << std::endl;
    }

    // Helper: diff a file (with optional string remap applied to old)
    auto diff_file = [&](PatchFileId id, const std::string& name,
                          std::function<void(std::vector<char>&, const StringRemapTable&)> remap_fn)
    {
        std::cerr << "  " << name << ": ";
        auto old_data = read_file(old_dir + "/" + name);
        auto new_data = read_file(new_dir + "/" + name);

        // Apply string remap to old data
        if (remap_fn) remap_fn(old_data, remap);

        // Compute zstd delta
        auto delta = zstd_delta_compress(old_data.data(), old_data.size(),
                                          new_data.data(), new_data.size());

        // Write section
        uint32_t fid = static_cast<uint32_t>(id);
        uint32_t enc = static_cast<uint32_t>(PatchEncoding::COPY_INSERT); // reusing enum for "delta"
        write_bytes(pf, &fid, 4);
        write_bytes(pf, &enc, 4);
        uint64_t os = old_data.size(), ns = new_data.size(), ds = delta.size();
        write_bytes(pf, &os, 8);
        write_bytes(pf, &ns, 8);
        write_bytes(pf, &ds, 8);
        pf.write(delta.data(), delta.size());

        double pct = ns > 0 ? ds * 100.0 / ns : 0;
        std::cerr << delta.size() << " bytes (" << std::fixed << std::setprecision(1) << pct
                  << "% of " << ns / 1024 / 1024 << " MiB)" << std::endl;
    };

    // Section: new strings.bin (full, as zstd delta from old)
    diff_file(PatchFileId::STRINGS, "strings.bin", nullptr);

    // Data files with string remap
    diff_file(PatchFileId::ADDR_POINTS, "addr_points.bin",
        [](std::vector<char>& d, const StringRemapTable& r) { apply_remap_addr_points(d, r); });
    diff_file(PatchFileId::STREET_WAYS, "street_ways.bin",
        [way_stride](std::vector<char>& d, const StringRemapTable& r) { apply_remap_ways(d, way_stride, r); });
    diff_file(PatchFileId::INTERP_WAYS, "interp_ways.bin",
        [interp_stride](std::vector<char>& d, const StringRemapTable& r) { apply_remap_interps(d, interp_stride, r); });
    diff_file(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin",
        [admin_stride](std::vector<char>& d, const StringRemapTable& r) { apply_remap_admin(d, admin_stride, r); });

    // Coordinate files (no string refs)
    std::function<void(std::vector<char>&, const StringRemapTable&)> no_remap;
    diff_file(PatchFileId::STREET_NODES, "street_nodes.bin", no_remap);
    diff_file(PatchFileId::INTERP_NODES, "interp_nodes.bin", no_remap);
    diff_file(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin", no_remap);

    // Index/entry files (no string refs, just positional data)
    diff_file(PatchFileId::GEO_CELLS, "geo_cells.bin", no_remap);
    diff_file(PatchFileId::STREET_ENTRIES, "street_entries.bin", no_remap);
    diff_file(PatchFileId::ADDR_ENTRIES, "addr_entries.bin", no_remap);
    diff_file(PatchFileId::INTERP_ENTRIES, "interp_entries.bin", no_remap);
    diff_file(PatchFileId::ADMIN_CELLS, "admin_cells.bin", no_remap);
    diff_file(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin", no_remap);

    // End marker
    uint32_t end = 0xFFFFFFFF;
    write_bytes(pf, &end, 4);

    auto total = pf.tellp();
    std::cerr << "\nTotal patch: " << total << " bytes (" << total / 1024 / 1024 << " MiB)" << std::endl;
    return 0;
}


The file /root/traccar-geocoder/builder/tools/geocoder_diff.cpp has been updated successfully.

// geocoder-diff: Compare two deterministic geocoder builds, produce a .gcpatch.
//
// Strategy:
// 1. Compute string remap (old string pool → new string pool)
// 2. For each data file: apply remap to old file, zstd-delta against new file
// 3. For files without string refs: zstd-delta directly
// 4. Package into .gcpatch
//
// Because builds are deterministic, applying this patch to the old build
// produces output byte-identical to the new build. Sequential patching works.
//
// Usage: geocoder-diff <old-dir> <new-dir> -o <patch-file>

#include <algorithm>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <functional>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

#include <zstd.h>
#include "patch_format.h"

// --- String remap ---

struct StringRemapTable {
    // Flat array indexed by old byte offset → new byte offset
    // Size = old pool size. Only populated at string start positions.
    std::vector<uint32_t> table;
    size_t old_pool_size = 0;

    void build(const std::vector<char>& old_pool, const std::vector<char>& new_pool) {
        old_pool_size = old_pool.size();
        // Index new pool: string → offset
        std::unordered_map<std::string, uint32_t> new_idx;
        size_t pos = 0;
        while (pos < new_pool.size()) {
            const char* s = new_pool.data() + pos;
            size_t len = strlen(s);
            new_idx[std::string(s, len)] = static_cast<uint32_t>(pos);
            pos += len + 1;
        }
        // Build remap
        table.resize(old_pool.size(), 0);
        pos = 0;
        while (pos < old_pool.size()) {
            const char* s = old_pool.data() + pos;
            size_t len = strlen(s);
            auto it = new_idx.find(std::string(s, len));
            table[pos] = (it != new_idx.end()) ? it->second : 0;
            pos += len + 1;
        }
    }

    uint32_t remap(uint32_t old_off) const {
        return (old_off < table.size()) ? table[old_off] : 0;
    }
};

// Apply string remap to specific fields in data files
static void apply_remap_addr_points(std::vector<char>& data, const StringRemapTable& rm) {
    for (size_t i = 0; i + 16 <= data.size(); i += 16) {
        for (size_t off : {8, 12}) {
            uint32_t v; memcpy(&v, data.data() + i + off, 4);
            v = rm.remap(v);
            memcpy(data.data() + i + off, &v, 4);
        }
    }
}

static void apply_remap_ways(std::vector<char>& data, size_t stride, const StringRemapTable& rm) {
    size_t name_off = (stride == 12) ? 8 : 5;
    for (size_t i = 0; i + stride <= data.size(); i += stride) {
        uint32_t v; memcpy(&v, data.data() + i + name_off, 4);
        v = rm.remap(v);
        memcpy(data.data() + i + name_off, &v, 4);
    }
}

static void apply_remap_interps(std::vector<char>& data, size_t stride, const StringRemapTable& rm) {
    size_t street_off = (stride >= 20) ? 8 : 5;
    for (size_t i = 0; i + stride <= data.size(); i += stride) {
        uint32_t v; memcpy(&v, data.data() + i + street_off, 4);
        v = rm.remap(v);
        memcpy(data.data() + i + street_off, &v, 4);
    }
}

static void apply_remap_admin(std::vector<char>& data, size_t stride, const StringRemapTable& rm) {
    for (size_t i = 0; i + stride <= data.size(); i += stride) {
        uint32_t v; memcpy(&v, data.data() + i + 8, 4); // name_id at offset 8
        v = rm.remap(v);
        memcpy(data.data() + i + 8, &v, 4);
    }
}

// --- zstd delta compression ---
// Uses zstd's dictionary mode: old file as dictionary, compress new file
// This produces very compact deltas when files are similar.

static std::vector<char> zstd_delta_compress(const char* old_data, size_t old_size,
                                               const char* new_data, size_t new_size) {
    // Create a compression context with the old file as a "prefix"
    ZSTD_CCtx* cctx = ZSTD_createCCtx();
    ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, 19);
    ZSTD_CCtx_setParameter(cctx, ZSTD_c_windowLog, 31); // 2 GiB window
    ZSTD_CCtx_setParameter(cctx, ZSTD_c_enableLongDistanceMatching, ZSTD_ps_enable);

    // Reference the old file as prefix (zstd matches against it)
    ZSTD_CCtx_refPrefix(cctx, old_data, old_size);

    size_t bound = ZSTD_compressBound(new_size);
    std::vector<char> out(bound);
    size_t result = ZSTD_compress2(cctx, out.data(), bound, new_data, new_size);
    ZSTD_freeCCtx(cctx);

    if (ZSTD_isError(result)) {
        std::cerr << "zstd compress error: " << ZSTD_getErrorName(result) << std::endl;
        return {};
    }
    out.resize(result);
    return out;
}

// --- Write helpers ---
static void write_bytes(std::ofstream& f, const void* data, size_t size) {
    f.write(reinterpret_cast<const char*>(data), size);
}

int main(int argc, char* argv[]) {
    if (argc < 5 || std::string(argv[3]) != "-o") {
        std::cerr << "Usage: geocoder-diff <old-dir> <new-dir> -o <patch-file>" << std::endl;
        return 1;
    }
    std::string old_dir = argv[1], new_dir = argv[2], patch_path = argv[4];

    // Load string pools, build remap
    std::cerr << "Building string remap..." << std::endl;
    auto old_strings = read_file(old_dir + "/strings.bin");
    auto new_strings = read_file(new_dir + "/strings.bin");
    StringRemapTable remap;
    remap.build(old_strings, new_strings);

    // Detect struct strides from old files
    auto old_ways_raw = read_file(old_dir + "/street_ways.bin");
    size_t way_stride = (old_ways_raw.size() % 12 == 0 && old_ways_raw.size() > 0) ? 12 : 9;
    auto old_interps_raw = read_file(old_dir + "/interp_ways.bin");
    size_t interp_stride = (old_interps_raw.size() % 20 == 0 && old_interps_raw.size() > 0) ? 20 : 18;
    auto old_admin_raw = read_file(old_dir + "/admin_polygons.bin");
    size_t admin_stride = 24;
    for (size_t s : {24, 20, 19}) {
        if (old_admin_raw.size() % s == 0 && old_admin_raw.size() > 0) { admin_stride = s; break; }
    }
    // Free raw data (will reload per-file)
    old_ways_raw.clear(); old_interps_raw.clear(); old_admin_raw.clear();

    // Open patch file
    std::ofstream pf(patch_path, std::ios::binary);
    pf.write(GCPATCH_MAGIC, 8);
    uint32_t ver = GCPATCH_VERSION, flags = 0;
    write_bytes(pf, &ver, 4);
    write_bytes(pf, &flags, 4);

    // Section: string remap table + new strings
    {
        std::cerr << "  Writing string remap..." << std::endl;
        uint32_t section_id = 0xFFFFFFFE; // remap marker
        write_bytes(pf, &section_id, 4);
        uint64_t old_pool_size = remap.old_pool_size;
        write_bytes(pf, &old_pool_size, 8);
        // Write the full remap table (compact: only entries at string start positions)
        // Format: count, then (old_off, new_off) pairs
        std::vector<std::pair<uint32_t, uint32_t>> entries;
        size_t pos = 0;
        while (pos < old_strings.size()) {
            entries.push_back({static_cast<uint32_t>(pos), remap.table[pos]});
            pos += strlen(old_strings.data() + pos) + 1;
        }
        uint32_t count = static_cast<uint32_t>(entries.size());
        write_bytes(pf, &count, 4);
        for (auto& [o, n] : entries) { write_bytes(pf, &o, 4); write_bytes(pf, &n, 4); }
        std::cerr << "    " << count << " remap entries" << std::endl;
    }

    // Helper: diff a file (with optional string remap applied to old)
    auto diff_file = [&](PatchFileId id, const std::string& name,
                          std::function<void(std::vector<char>&, const StringRemapTable&)> remap_fn)
    {
        std::cerr << "  " << name << ": ";
        auto old_data = read_file(old_dir + "/" + name);
        auto new_data = read_file(new_dir + "/" + name);

        // Apply string remap to old data
        if (remap_fn) remap_fn(old_data, remap);

        // Compute zstd delta
        auto delta = zstd_delta_compress(old_data.data(), old_data.size(),
                                          new_data.data(), new_data.size());

        // Write section
        uint32_t fid = static_cast<uint32_t>(id);
        uint32_t enc = static_cast<uint32_t>(PatchEncoding::COPY_INSERT); // reusing enum for "delta"
        write_bytes(pf, &fid, 4);
        write_bytes(pf, &enc, 4);
        uint64_t os = old_data.size(), ns = new_data.size(), ds = delta.size();
        write_bytes(pf, &os, 8);
        write_bytes(pf, &ns, 8);
        write_bytes(pf, &ds, 8);
        pf.write(delta.data(), delta.size());

        double pct = ns > 0 ? ds * 100.0 / ns : 0;
        std::cerr << delta.size() << " bytes (" << std::fixed << std::setprecision(1) << pct
                  << "% of " << ns / 1024 / 1024 << " MiB)" << std::endl;
    };

    // Section: new strings.bin (full, as zstd delta from old)
    diff_file(PatchFileId::STRINGS, "strings.bin", nullptr);

    // Data files with string remap
    diff_file(PatchFileId::ADDR_POINTS, "addr_points.bin",
        [](std::vector<char>& d, const StringRemapTable& r) { apply_remap_addr_points(d, r); });
    diff_file(PatchFileId::STREET_WAYS, "street_ways.bin",
        [way_stride](std::vector<char>& d, const StringRemapTable& r) { apply_remap_ways(d, way_stride, r); });
    diff_file(PatchFileId::INTERP_WAYS, "interp_ways.bin",
        [interp_stride](std::vector<char>& d, const StringRemapTable& r) { apply_remap_interps(d, interp_stride, r); });
    diff_file(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin",
        [admin_stride](std::vector<char>& d, const StringRemapTable& r) { apply_remap_admin(d, admin_stride, r); });

    // Coordinate files (no string refs)
    std::function<void(std::vector<char>&, const StringRemapTable&)> no_remap;
    diff_file(PatchFileId::STREET_NODES, "street_nodes.bin", no_remap);
    diff_file(PatchFileId::INTERP_NODES, "interp_nodes.bin", no_remap);
    diff_file(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin", no_remap);

    // Index/entry files (no string refs, just positional data)
    diff_file(PatchFileId::GEO_CELLS, "geo_cells.bin", no_remap);
    diff_file(PatchFileId::STREET_ENTRIES, "street_entries.bin", no_remap);
    diff_file(PatchFileId::ADDR_ENTRIES, "addr_entries.bin", no_remap);
    diff_file(PatchFileId::INTERP_ENTRIES, "interp_entries.bin", no_remap);
    diff_file(PatchFileId::ADMIN_CELLS, "admin_cells.bin", no_remap);
    diff_file(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin", no_remap);

    // End marker
    uint32_t end = 0xFFFFFFFF;
    write_bytes(pf, &end, 4);

    auto total = pf.tellp();
    std::cerr << "\nTotal patch: " << total << " bytes (" << total / 1024 / 1024 << " MiB)" << std::endl;
    return 0;
}

-// geocoder-diff: Compare two canonical geocoder indexes and produce a compact patch.

+// geocoder-diff: Compare two deterministic geocoder builds, produce a .gcpatch.

-// 1. Build string remap (old string offsets → new string offsets)

-// 2. For data files with string refs: apply remap to old file in memory, then binary diff

-// 3. For coordinate files: direct binary diff (no string refs)

-// 4. For small derived files: full replacement (compressed)

+// 1. Compute string remap (old string pool → new string pool)

+// 2. For each data file: apply remap to old file, zstd-delta against new file

+// 3. For files without string refs: zstd-delta directly

-// The binary diff uses a simple block-based COPY/INSERT encoder:

-// - Compare 64-byte blocks between (remapped) old and new

-// - COPY for matching blocks, INSERT for different blocks

-// - Handles size differences (new may be larger)

+// Because builds are deterministic, applying this patch to the old build

+// produces output byte-identical to the new build. Sequential patching works.

-// Usage: geocoder-diff <old-canonical-dir> <new-canonical-dir> -o <patch-file>

+// Usage: geocoder-diff <old-dir> <new-dir> -o <patch-file>

-static std::vector<char> zlib_compress(const char* data, size_t size) {

-    uLongf bound = compressBound(size);

-    int ret = compress2(reinterpret_cast<Bytef*>(out.data()), &bound,

-                        reinterpret_cast<const Bytef*>(data), size, 9);

-static std::unordered_map<uint32_t, uint32_t> build_string_remap(

-    const std::vector<char>& old_pool, const std::vector<char>& new_pool)

-    std::unordered_map<std::string, uint32_t> new_index;

-        const char* s = new_pool.data() + pos;

-        new_index[std::string(s, len)] = static_cast<uint32_t>(pos);

+    // Flat array indexed by old byte offset → new byte offset

+    // Size = old pool size. Only populated at string start positions.

-    std::unordered_map<uint32_t, uint32_t> remap;

-        const char* s = old_pool.data() + pos;

-        auto it = new_index.find(std::string(s, len));

-            remap[static_cast<uint32_t>(pos)] = it->second;

+    void build(const std::vector<char>& old_pool, const std::vector<char>& new_pool) {

+        old_pool_size = old_pool.size();

+        // Index new pool: string → offset

+        std::unordered_map<std::string, uint32_t> new_idx;

+        while (pos < new_pool.size()) {

+            const char* s = new_pool.data() + pos;

+            new_idx[std::string(s, len)] = static_cast<uint32_t>(pos);

+        table.resize(old_pool.size(), 0);

+        while (pos < old_pool.size()) {

+            const char* s = old_pool.data() + pos;

+            auto it = new_idx.find(std::string(s, len));

+            table[pos] = (it != new_idx.end()) ? it->second : 0;

-// Apply string remap to specific fields in a data buffer

-static void remap_addr_points(std::vector<char>& data,

-                                const std::unordered_map<uint32_t, uint32_t>& remap) {

-        char* p = data.data() + i * 16;

-        // housenumber_id at offset 8, street_id at offset 12

+    uint32_t remap(uint32_t old_off) const {

+        return (old_off < table.size()) ? table[old_off] : 0;

+// Apply string remap to specific fields in data files

+static void apply_remap_addr_points(std::vector<char>& data, const StringRemapTable& rm) {

-            memcpy(&old_id, p + off, 4);

-            auto it = remap.find(old_id);

-                memcpy(p + off, &it->second, 4);

+            uint32_t v; memcpy(&v, data.data() + i + off, 4);

+            memcpy(data.data() + i + off, &v, 4);

-static void remap_ways(std::vector<char>& data, size_t stride,

-                        const std::unordered_map<uint32_t, uint32_t>& remap) {

+static void apply_remap_ways(std::vector<char>& data, size_t stride, const StringRemapTable& rm) {

size_t name_off = (stride == 12) ? 8 : 5;

-        char* p = data.data() + i * stride;

-        memcpy(&old_id, p + name_off, 4);

-            memcpy(p + name_off, &it->second, 4);

+    for (size_t i = 0; i + stride <= data.size(); i += stride) {

+        uint32_t v; memcpy(&v, data.data() + i + name_off, 4);

+        memcpy(data.data() + i + name_off, &v, 4);

-static void remap_interps(std::vector<char>& data, size_t stride,

-                           const std::unordered_map<uint32_t, uint32_t>& remap) {

-    size_t street_off = (stride == 20) ? 8 : 5;

-        memcpy(&old_id, p + street_off, 4);

-            memcpy(p + street_off, &it->second, 4);

+static void apply_remap_interps(std::vector<char>& data, size_t stride, const StringRemapTable& rm) {

+    size_t street_off = (stride >= 20) ? 8 : 5;

+        uint32_t v; memcpy(&v, data.data() + i + street_off, 4);

+        memcpy(data.data() + i + street_off, &v, 4);

-static void remap_admin_polygons(std::vector<char>& data, size_t stride,

-                                   const std::unordered_map<uint32_t, uint32_t>& remap) {

-        char* p = data.data() + i * stride;

-        memcpy(&old_id, p + 8, 4); // name_id at offset 8

-            memcpy(p + 8, &it->second, 4);

+static void apply_remap_admin(std::vector<char>& data, size_t stride, const StringRemapTable& rm) {

+    for (size_t i = 0; i + stride <= data.size(); i += stride) {

+        uint32_t v; memcpy(&v, data.data() + i + 8, 4); // name_id at offset 8

+        memcpy(data.data() + i + 8, &v, 4);

-// --- Block-based COPY/INSERT binary diff ---

-// Compares (remapped) old vs new, produces a compact delta.

-// Block size of 64 bytes balances granularity vs overhead.

+// Uses zstd's dictionary mode: old file as dictionary, compress new file

+// This produces very compact deltas when files are similar.

-static constexpr size_t DIFF_BLOCK = 64;

+static std::vector<char> zstd_delta_compress(const char* old_data, size_t old_size,

+                                               const char* new_data, size_t new_size) {

+    // Create a compression context with the old file as a "prefix"

+    ZSTD_CCtx* cctx = ZSTD_createCCtx();

+    ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, 19);

+    ZSTD_CCtx_setParameter(cctx, ZSTD_c_windowLog, 31); // 2 GiB window

+    ZSTD_CCtx_setParameter(cctx, ZSTD_c_enableLongDistanceMatching, ZSTD_ps_enable);

-static std::vector<char> compute_block_diff(const std::vector<char>& old_data,

-                                              const std::vector<char>& new_data) {

+    // Reference the old file as prefix (zstd matches against it)

+    ZSTD_CCtx_refPrefix(cctx, old_data, old_size);

-    while (new_pos < new_data.size()) {

-        // Check if current block matches

-        size_t remaining_new = new_data.size() - new_pos;

-        size_t remaining_old = (old_pos < old_data.size()) ? old_data.size() - old_pos : 0;

-        size_t block = std::min(DIFF_BLOCK, remaining_new);

+    size_t bound = ZSTD_compressBound(new_size);

+    size_t result = ZSTD_compress2(cctx, out.data(), bound, new_data, new_size);

-            memcmp(old_data.data() + old_pos, new_data.data() + new_pos, block) == 0) {

-            // Find full extent of match

-            while (old_pos + match_len < old_data.size() &&

-                   new_pos + match_len < new_data.size() &&

-                   old_data[old_pos + match_len] == new_data[new_pos + match_len]) {

-            // Emit COPY: tag(1) + offset(8) + length(4) = 13 bytes

-            uint32_t len = static_cast<uint32_t>(match_len);

-            ops.insert(ops.end(), reinterpret_cast<const char*>(&off),

-                      reinterpret_cast<const char*>(&off) + 8);

-            ops.insert(ops.end(), reinterpret_cast<const char*>(&len),

-                      reinterpret_cast<const char*>(&len) + 4);

-            // Find extent of non-match (up to next matching block)

-            size_t insert_start = new_pos;

-            old_scan += std::min(block, remaining_old);

-            // Keep scanning until we find a matching block again

-            while (new_pos < new_data.size()) {

-                size_t rem_new = new_data.size() - new_pos;

-                size_t rem_old = (old_scan < old_data.size()) ? old_data.size() - old_scan : 0;

-                size_t b = std::min(DIFF_BLOCK, rem_new);

-                    memcmp(old_data.data() + old_scan, new_data.data() + new_pos, b) == 0) {

-                    break; // Found match

-                old_scan += std::min(b, rem_old);

-            // Emit INSERT: tag(1) + length(4) + data

-            size_t insert_len = new_pos - insert_start;

-            uint32_t len = static_cast<uint32_t>(insert_len);

-            ops.insert(ops.end(), new_data.data() + insert_start,

-                      new_data.data() + insert_start + insert_len);

+        std::cerr << "zstd compress error: " << ZSTD_getErrorName(result) << std::endl;

-static void write_u32(std::ofstream& f, uint32_t v) { f.write(reinterpret_cast<const char*>(&v), 4); }

-static void write_u64(std::ofstream& f, uint64_t v) { f.write(reinterpret_cast<const char*>(&v), 8); }

+static void write_bytes(std::ofstream& f, const void* data, size_t size) {

+    f.write(reinterpret_cast<const char*>(data), size);

if (argc < 5 || std::string(argv[3]) != "-o") {

+    std::string old_dir = argv[1], new_dir = argv[2], patch_path = argv[4];

-    std::cerr << "Loading string pools..." << std::endl;

+    std::cerr << "Building string remap..." << std::endl;

auto old_strings = read_file(old_dir + "/strings.bin");

auto new_strings = read_file(new_dir + "/strings.bin");

-    auto remap = build_string_remap(old_strings, new_strings);

-    std::cerr << "  Remap: " << remap.size() << " strings mapped" << std::endl;

+    remap.build(old_strings, new_strings);

+    // Detect struct strides from old files

+    auto old_ways_raw = read_file(old_dir + "/street_ways.bin");

+    size_t way_stride = (old_ways_raw.size() % 12 == 0 && old_ways_raw.size() > 0) ? 12 : 9;

+    auto old_interps_raw = read_file(old_dir + "/interp_ways.bin");

+    size_t interp_stride = (old_interps_raw.size() % 20 == 0 && old_interps_raw.size() > 0) ? 20 : 18;

+    auto old_admin_raw = read_file(old_dir + "/admin_polygons.bin");

+        if (old_admin_raw.size() % s == 0 && old_admin_raw.size() > 0) { admin_stride = s; break; }

+    // Free raw data (will reload per-file)

+    old_ways_raw.clear(); old_interps_raw.clear(); old_admin_raw.clear();

+    uint32_t ver = GCPATCH_VERSION, flags = 0;

-    // Section: strings.bin (full replacement)

+    // Section: string remap table + new strings

-        std::cerr << "  strings.bin: full replacement..." << std::endl;

-        auto compressed = zlib_compress(new_strings.data(), new_strings.size());

-        write_u32(pf, static_cast<uint32_t>(PatchFileId::STRINGS));

-        write_u32(pf, static_cast<uint32_t>(PatchEncoding::RAW_REPLACE));

-        write_u64(pf, new_strings.size());

-        write_u64(pf, compressed.size());

-        pf.write(compressed.data(), compressed.size());

-        std::cerr << "    " << new_strings.size() << " -> " << compressed.size() << std::endl;

+        std::cerr << "  Writing string remap..." << std::endl;

+        uint32_t section_id = 0xFFFFFFFE; // remap marker

+        write_bytes(pf, &section_id, 4);

+        uint64_t old_pool_size = remap.old_pool_size;

+        write_bytes(pf, &old_pool_size, 8);

+        // Write the full remap table (compact: only entries at string start positions)

+        // Format: count, then (old_off, new_off) pairs

+        std::vector<std::pair<uint32_t, uint32_t>> entries;

+        while (pos < old_strings.size()) {

+            entries.push_back({static_cast<uint32_t>(pos), remap.table[pos]});

+            pos += strlen(old_strings.data() + pos) + 1;

+        uint32_t count = static_cast<uint32_t>(entries.size());

+        for (auto& [o, n] : entries) { write_bytes(pf, &o, 4); write_bytes(pf, &n, 4); }

+        std::cerr << "    " << count << " remap entries" << std::endl;

-    // Helper: diff a file with optional string remapping

+    // Helper: diff a file (with optional string remap applied to old)

auto diff_file = [&](PatchFileId id, const std::string& name,

-                          std::function<void(std::vector<char>&)> remap_fn) {

+                          std::function<void(std::vector<char>&, const StringRemapTable&)> remap_fn)

auto old_data = read_file(old_dir + "/" + name);

auto new_data = read_file(new_dir + "/" + name);

-        // Apply string remap to old data (makes it comparable to new)

+        // Apply string remap to old data

+        if (remap_fn) remap_fn(old_data, remap);

-        auto ops = compute_block_diff(old_data, new_data);

+        auto delta = zstd_delta_compress(old_data.data(), old_data.size(),

+                                          new_data.data(), new_data.size());

-        auto compressed = zlib_compress(ops.data(), ops.size());

+        uint32_t fid = static_cast<uint32_t>(id);

+        uint32_t enc = static_cast<uint32_t>(PatchEncoding::COPY_INSERT); // reusing enum for "delta"

+        uint64_t os = old_data.size(), ns = new_data.size(), ds = delta.size();

+        pf.write(delta.data(), delta.size());

-        // Also compute full replacement size for comparison

-        auto full_compressed = zlib_compress(new_data.data(), new_data.size());

-        bool use_delta = compressed.size() < full_compressed.size();

-        write_u32(pf, static_cast<uint32_t>(id));

-            write_u32(pf, static_cast<uint32_t>(PatchEncoding::COPY_INSERT));

-            write_u64(pf, old_data.size()); // old size

-            write_u64(pf, new_data.size()); // new size

-            write_u64(pf, compressed.size());

-            pf.write(compressed.data(), compressed.size());

-            std::cerr << "delta " << compressed.size() << " bytes";

-            write_u32(pf, static_cast<uint32_t>(PatchEncoding::RAW_REPLACE));

-            write_u64(pf, new_data.size());

-            write_u64(pf, full_compressed.size());

-            pf.write(full_compressed.data(), full_compressed.size());

-            std::cerr << "full " << full_compressed.size() << " bytes";

-        double pct = (new_data.size() > 0) ?

-            (use_delta ? compressed.size() : full_compressed.size()) * 100.0 / new_data.size() : 0;

-        std::cerr << " (" << std::fixed << std::setprecision(1) << pct << "% of "

+        double pct = ns > 0 ? ds * 100.0 / ns : 0;

+        std::cerr << delta.size() << " bytes (" << std::fixed << std::setprecision(1) << pct

-    size_t way_stride = detect_way_stride(old_dir + "/street_ways.bin");

-    auto old_interp_data = read_file(old_dir + "/interp_ways.bin");

-    size_t interp_stride = (old_interp_data.size() % 20 == 0 && old_interp_data.size() > 0) ? 20 : 18;

-    auto old_admin_data = read_file(old_dir + "/admin_polygons.bin");

-        if (old_admin_data.size() % s == 0 && old_admin_data.size() > 0) { admin_stride = s; break; }

+    // Section: new strings.bin (full, as zstd delta from old)

+    diff_file(PatchFileId::STRINGS, "strings.bin", nullptr);

-    // Data files with string remapping

-        [&](std::vector<char>& d) { remap_addr_points(d, remap); });

+        [](std::vector<char>& d, const StringRemapTable& r) { apply_remap_addr_points(d, r); });

-        [&, way_stride](std::vector<char>& d) { remap_ways(d, way_stride, remap); });

+        [way_stride](std::vector<char>& d, const StringRemapTable& r) { apply_remap_ways(d, way_stride, r); });

-        [&, interp_stride](std::vector<char>& d) { remap_interps(d, interp_stride, remap); });

+        [interp_stride](std::vector<char>& d, const StringRemapTable& r) { apply_remap_interps(d, interp_stride, r); });

-        [&, admin_stride](std::vector<char>& d) { remap_admin_polygons(d, admin_stride, remap); });

+        [admin_stride](std::vector<char>& d, const StringRemapTable& r) { apply_remap_admin(d, admin_stride, r); });

-    // Coordinate files (no string refs, direct diff)

-    std::function<void(std::vector<char>&)> no_remap;

+    // Coordinate files (no string refs)

+    std::function<void(std::vector<char>&, const StringRemapTable&)> no_remap;

-    // Derived/index files (no remap needed, direct diff)

+    // Index/entry files (no string refs, just positional data)

-    write_u32(pf, 0xFFFFFFFF); // end marker

// geocoder-diff: Compare two canonical geocoder indexes and produce a compact patch.
//
// Strategy:
// 1. Build string remap (old string offsets → new string offsets)
// 2. For data files with string refs: apply remap to old file in memory, then binary diff
// 3. For coordinate files: direct binary diff (no string refs)
// 4. For small derived files: full replacement (compressed)
//
// The binary diff uses a simple block-based COPY/INSERT encoder:
// - Compare 64-byte blocks between (remapped) old and new
// - COPY for matching blocks, INSERT for different blocks
// - Handles size differences (new may be larger)
//
// Usage: geocoder-diff <old-canonical-dir> <new-canonical-dir> -o <patch-file>

#include <algorithm>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <functional>
#include <iomanip>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
#include <zlib.h>

#include "patch_format.h"

// --- zlib helpers ---

static std::vector<char> zlib_compress(const char* data, size_t size) {
    if (size == 0) return {};
    uLongf bound = compressBound(size);
    std::vector<char> out(bound);
    int ret = compress2(reinterpret_cast<Bytef*>(out.data()), &bound,
                        reinterpret_cast<const Bytef*>(data), size, 9);
    if (ret != Z_OK) return {};
    out.resize(bound);
    return out;
}

// --- String remap ---

static std::unordered_map<uint32_t, uint32_t> build_string_remap(
    const std::vector<char>& old_pool, const std::vector<char>& new_pool)
{
    // Index new pool
    std::unordered_map<std::string, uint32_t> new_index;
    size_t pos = 0;
    while (pos < new_pool.size()) {
        const char* s = new_pool.data() + pos;
        size_t len = strlen(s);
        new_index[std::string(s, len)] = static_cast<uint32_t>(pos);
        pos += len + 1;
    }

    // Map old offsets → new offsets
    std::unordered_map<uint32_t, uint32_t> remap;
    pos = 0;
    while (pos < old_pool.size()) {
        const char* s = old_pool.data() + pos;
        size_t len = strlen(s);
        auto it = new_index.find(std::string(s, len));
        if (it != new_index.end()) {
            remap[static_cast<uint32_t>(pos)] = it->second;
        }
        pos += len + 1;
    }
    return remap;
}

// Apply string remap to specific fields in a data buffer
static void remap_addr_points(std::vector<char>& data,
                                const std::unordered_map<uint32_t, uint32_t>& remap) {
    size_t n = data.size() / 16;
    for (size_t i = 0; i < n; i++) {
        char* p = data.data() + i * 16;
        // housenumber_id at offset 8, street_id at offset 12
        for (size_t off : {8, 12}) {
            uint32_t old_id;
            memcpy(&old_id, p + off, 4);
            auto it = remap.find(old_id);
            if (it != remap.end()) {
                memcpy(p + off, &it->second, 4);
            }
        }
    }
}

static void remap_ways(std::vector<char>& data, size_t stride,
                        const std::unordered_map<uint32_t, uint32_t>& remap) {
    size_t n = data.size() / stride;
    size_t name_off = (stride == 12) ? 8 : 5;
    for (size_t i = 0; i < n; i++) {
        char* p = data.data() + i * stride;
        uint32_t old_id;
        memcpy(&old_id, p + name_off, 4);
        auto it = remap.find(old_id);
        if (it != remap.end()) {
            memcpy(p + name_off, &it->second, 4);
        }
    }
}

static void remap_interps(std::vector<char>& data, size_t stride,
                           const std::unordered_map<uint32_t, uint32_t>& remap) {
    size_t n = data.size() / stride;
    size_t street_off = (stride == 20) ? 8 : 5;
    for (size_t i = 0; i < n; i++) {
        char* p = data.data() + i * stride;
        uint32_t old_id;
        memcpy(&old_id, p + street_off, 4);
        auto it = remap.find(old_id);
        if (it != remap.end()) {
            memcpy(p + street_off, &it->second, 4);
        }
    }
}

static void remap_admin_polygons(std::vector<char>& data, size_t stride,
                                   const std::unordered_map<uint32_t, uint32_t>& remap) {
    size_t n = data.size() / stride;
    for (size_t i = 0; i < n; i++) {
        char* p = data.data() + i * stride;
        uint32_t old_id;
        memcpy(&old_id, p + 8, 4); // name_id at offset 8
        auto it = remap.find(old_id);
        if (it != remap.end()) {
            memcpy(p + 8, &it->second, 4);
        }
    }
}

// --- Block-based COPY/INSERT binary diff ---
// Compares (remapped) old vs new, produces a compact delta.
// Block size of 64 bytes balances granularity vs overhead.

static constexpr size_t DIFF_BLOCK = 64;

static std::vector<char> compute_block_diff(const std::vector<char>& old_data,
                                              const std::vector<char>& new_data) {
    std::vector<char> ops;
    size_t old_pos = 0, new_pos = 0;

    while (new_pos < new_data.size()) {
        // Check if current block matches
        size_t remaining_new = new_data.size() - new_pos;
        size_t remaining_old = (old_pos < old_data.size()) ? old_data.size() - old_pos : 0;
        size_t block = std::min(DIFF_BLOCK, remaining_new);

        if (remaining_old >= block &&
            memcmp(old_data.data() + old_pos, new_data.data() + new_pos, block) == 0) {
            // Find full extent of match
            size_t match_len = block;
            while (old_pos + match_len < old_data.size() &&
                   new_pos + match_len < new_data.size() &&
                   old_data[old_pos + match_len] == new_data[new_pos + match_len]) {
                match_len++;
            }
            // Emit COPY: tag(1) + offset(8) + length(4) = 13 bytes
            ops.push_back(OP_COPY);
            uint64_t off = old_pos;
            uint32_t len = static_cast<uint32_t>(match_len);
            ops.insert(ops.end(), reinterpret_cast<const char*>(&off),
                      reinterpret_cast<const char*>(&off) + 8);
            ops.insert(ops.end(), reinterpret_cast<const char*>(&len),
                      reinterpret_cast<const char*>(&len) + 4);
            old_pos += match_len;
            new_pos += match_len;
        } else {
            // Find extent of non-match (up to next matching block)
            size_t insert_start = new_pos;
            size_t old_scan = old_pos;

            // Advance by one block
            new_pos += block;
            old_scan += std::min(block, remaining_old);

            // Keep scanning until we find a matching block again
            while (new_pos < new_data.size()) {
                size_t rem_new = new_data.size() - new_pos;
                size_t rem_old = (old_scan < old_data.size()) ? old_data.size() - old_scan : 0;
                size_t b = std::min(DIFF_BLOCK, rem_new);
                if (rem_old >= b &&
                    memcmp(old_data.data() + old_scan, new_data.data() + new_pos, b) == 0) {
                    break; // Found match
                }
                new_pos += b;
                old_scan += std::min(b, rem_old);
            }

            // Emit INSERT: tag(1) + length(4) + data
            size_t insert_len = new_pos - insert_start;
            ops.push_back(OP_INSERT);
            uint32_t len = static_cast<uint32_t>(insert_len);
            ops.insert(ops.end(), reinterpret_cast<const char*>(&len),
                      reinterpret_cast<const char*>(&len) + 4);
            ops.insert(ops.end(), new_data.data() + insert_start,
                      new_data.data() + insert_start + insert_len);
            old_pos = old_scan;
        }
    }
    return ops;
}

// --- Write helpers ---
static void write_u32(std::ofstream& f, uint32_t v) { f.write(reinterpret_cast<const char*>(&v), 4); }
static void write_u64(std::ofstream& f, uint64_t v) { f.write(reinterpret_cast<const char*>(&v), 8); }

int main(int argc, char* argv[]) {
    if (argc < 5 || std::string(argv[3]) != "-o") {
        std::cerr << "Usage: geocoder-diff <old-dir> <new-dir> -o <patch-file>" << std::endl;
        return 1;
    }

    std::string old_dir = argv[1];
    std::string new_dir = argv[2];
    std::string patch_path = argv[4];

    std::cerr << "Loading string pools..." << std::endl;
    auto old_strings = read_file(old_dir + "/strings.bin");
    auto new_strings = read_file(new_dir + "/strings.bin");
    auto remap = build_string_remap(old_strings, new_strings);
    std::cerr << "  Remap: " << remap.size() << " strings mapped" << std::endl;

    std::ofstream pf(patch_path, std::ios::binary);
    pf.write(GCPATCH_MAGIC, 8);
    write_u32(pf, GCPATCH_VERSION);
    write_u32(pf, 0);

    // Section: strings.bin (full replacement)
    {
        std::cerr << "  strings.bin: full replacement..." << std::endl;
        auto compressed = zlib_compress(new_strings.data(), new_strings.size());
        write_u32(pf, static_cast<uint32_t>(PatchFileId::STRINGS));
        write_u32(pf, static_cast<uint32_t>(PatchEncoding::RAW_REPLACE));
        write_u64(pf, new_strings.size());
        write_u64(pf, compressed.size());
        pf.write(compressed.data(), compressed.size());
        std::cerr << "    " << new_strings.size() << " -> " << compressed.size() << std::endl;
    }

    // Helper: diff a file with optional string remapping
    auto diff_file = [&](PatchFileId id, const std::string& name,
                          std::function<void(std::vector<char>&)> remap_fn) {
        std::cerr << "  " << name << ": ";
        auto old_data = read_file(old_dir + "/" + name);
        auto new_data = read_file(new_dir + "/" + name);

        // Apply string remap to old data (makes it comparable to new)
        if (remap_fn) {
            remap_fn(old_data);
        }

        // Compute block diff
        auto ops = compute_block_diff(old_data, new_data);

        // Compress the ops stream
        auto compressed = zlib_compress(ops.data(), ops.size());

        // Also compute full replacement size for comparison
        auto full_compressed = zlib_compress(new_data.data(), new_data.size());

        // Use whichever is smaller
        bool use_delta = compressed.size() < full_compressed.size();

        write_u32(pf, static_cast<uint32_t>(id));
        if (use_delta) {
            write_u32(pf, static_cast<uint32_t>(PatchEncoding::COPY_INSERT));
            write_u64(pf, old_data.size()); // old size
            write_u64(pf, new_data.size()); // new size
            write_u64(pf, compressed.size());
            pf.write(compressed.data(), compressed.size());
            std::cerr << "delta " << compressed.size() << " bytes";
        } else {
            write_u32(pf, static_cast<uint32_t>(PatchEncoding::RAW_REPLACE));
            write_u64(pf, new_data.size());
            write_u64(pf, full_compressed.size());
            pf.write(full_compressed.data(), full_compressed.size());
            std::cerr << "full " << full_compressed.size() << " bytes";
        }
        double pct = (new_data.size() > 0) ?
            (use_delta ? compressed.size() : full_compressed.size()) * 100.0 / new_data.size() : 0;
        std::cerr << " (" << std::fixed << std::setprecision(1) << pct << "% of "
                  << new_data.size() / 1024 / 1024 << " MiB)" << std::endl;
    };

    // Detect strides for struct files
    size_t way_stride = detect_way_stride(old_dir + "/street_ways.bin");
    auto old_interp_data = read_file(old_dir + "/interp_ways.bin");
    size_t interp_stride = (old_interp_data.size() % 20 == 0 && old_interp_data.size() > 0) ? 20 : 18;
    auto old_admin_data = read_file(old_dir + "/admin_polygons.bin");
    size_t admin_stride = 24;
    for (size_t s : {24, 20, 19}) {
        if (old_admin_data.size() % s == 0 && old_admin_data.size() > 0) { admin_stride = s; break; }
    }

    // Data files with string remapping
    diff_file(PatchFileId::ADDR_POINTS, "addr_points.bin",
        [&](std::vector<char>& d) { remap_addr_points(d, remap); });

    diff_file(PatchFileId::STREET_WAYS, "street_ways.bin",
        [&, way_stride](std::vector<char>& d) { remap_ways(d, way_stride, remap); });

    diff_file(PatchFileId::INTERP_WAYS, "interp_ways.bin",
        [&, interp_stride](std::vector<char>& d) { remap_interps(d, interp_stride, remap); });

    diff_file(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin",
        [&, admin_stride](std::vector<char>& d) { remap_admin_polygons(d, admin_stride, remap); });

    // Coordinate files (no string refs, direct diff)
    std::function<void(std::vector<char>&)> no_remap;
    diff_file(PatchFileId::STREET_NODES, "street_nodes.bin", no_remap);
    diff_file(PatchFileId::INTERP_NODES, "interp_nodes.bin", no_remap);
    diff_file(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin", no_remap);

    // Derived/index files (no remap needed, direct diff)
    diff_file(PatchFileId::GEO_CELLS, "geo_cells.bin", no_remap);
    diff_file(PatchFileId::STREET_ENTRIES, "street_entries.bin", no_remap);
    diff_file(PatchFileId::ADDR_ENTRIES, "addr_entries.bin", no_remap);
    diff_file(PatchFileId::INTERP_ENTRIES, "interp_entries.bin", no_remap);
    diff_file(PatchFileId::ADMIN_CELLS, "admin_cells.bin", no_remap);
    diff_file(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin", no_remap);

    write_u32(pf, 0xFFFFFFFF); // end marker
    auto total = pf.tellp();
    std::cerr << "\nTotal patch: " << total << " bytes (" << total / 1024 / 1024 << " MiB)" << std::endl;
    return 0;
}


Now write the patch applicator:


// geocoder-patch: Apply a .gcpatch to a geocoder index directory.
//
// For each file: reads old file, applies string remap (if applicable),
// then applies zstd delta decompression to produce the new file.
//
// Usage: geocoder-patch <current-dir> <patch-file> -o <output-dir>

#include <algorithm>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

#include <zstd.h>
#include "patch_format.h"

// --- String remap application ---
// Same functions as the diff tool, applied to old files before delta decompression.

static void apply_remap_addr_points(std::vector<char>& data,
                                      const std::vector<std::pair<uint32_t,uint32_t>>& entries) {
    std::unordered_map<uint32_t, uint32_t> rm;
    rm.reserve(entries.size());
    for (auto& [o, n] : entries) rm[o] = n;
    for (size_t i = 0; i + 16 <= data.size(); i += 16) {
        for (size_t off : {8, 12}) {
            uint32_t v; memcpy(&v, data.data() + i + off, 4);
            auto it = rm.find(v);
            if (it != rm.end()) { memcpy(data.data() + i + off, &it->second, 4); }
        }
    }
}

static void apply_remap_ways(std::vector<char>& data, size_t stride,
                               const std::vector<std::pair<uint32_t,uint32_t>>& entries) {
    std::unordered_map<uint32_t, uint32_t> rm;
    rm.reserve(entries.size());
    for (auto& [o, n] : entries) rm[o] = n;
    size_t name_off = (stride == 12) ? 8 : 5;
    for (size_t i = 0; i + stride <= data.size(); i += stride) {
        uint32_t v; memcpy(&v, data.data() + i + name_off, 4);
        auto it = rm.find(v);
        if (it != rm.end()) memcpy(data.data() + i + name_off, &it->second, 4);
    }
}

static void apply_remap_interps(std::vector<char>& data, size_t stride,
                                  const std::vector<std::pair<uint32_t,uint32_t>>& entries) {
    std::unordered_map<uint32_t, uint32_t> rm;
    rm.reserve(entries.size());
    for (auto& [o, n] : entries) rm[o] = n;
    size_t street_off = (stride >= 20) ? 8 : 5;
    for (size_t i = 0; i + stride <= data.size(); i += stride) {
        uint32_t v; memcpy(&v, data.data() + i + street_off, 4);
        auto it = rm.find(v);
        if (it != rm.end()) memcpy(data.data() + i + street_off, &it->second, 4);
    }
}

static void apply_remap_admin(std::vector<char>& data, size_t stride,
                                const std::vector<std::pair<uint32_t,uint32_t>>& entries) {
    std::unordered_map<uint32_t, uint32_t> rm;
    rm.reserve(entries.size());
    for (auto& [o, n] : entries) rm[o] = n;
    for (size_t i = 0; i + stride <= data.size(); i += stride) {
        uint32_t v; memcpy(&v, data.data() + i + 8, 4);
        auto it = rm.find(v);
        if (it != rm.end()) memcpy(data.data() + i + 8, &it->second, 4);
    }
}

// --- zstd delta decompression ---

static std::vector<char> zstd_delta_decompress(const char* prefix_data, size_t prefix_size,
                                                 const char* delta_data, size_t delta_size,
                                                 size_t expected_size) {
    ZSTD_DCtx* dctx = ZSTD_createDCtx();
    ZSTD_DCtx_setParameter(dctx, ZSTD_d_windowLogMax, 31);
    ZSTD_DCtx_refPrefix(dctx, prefix_data, prefix_size);

    std::vector<char> out(expected_size);
    size_t result = ZSTD_decompressDCtx(dctx, out.data(), expected_size, delta_data, delta_size);
    ZSTD_freeDCtx(dctx);

    if (ZSTD_isError(result)) {
        std::cerr << "zstd decompress error: " << ZSTD_getErrorName(result) << std::endl;
        return {};
    }
    out.resize(result);
    return out;
}

int main(int argc, char* argv[]) {
    if (argc < 5 || std::string(argv[3]) != "-o") {
        std::cerr << "Usage: geocoder-patch <current-dir> <patch-file> -o <output-dir>" << std::endl;
        return 1;
    }
    std::string cur_dir = argv[1], patch_path = argv[2], out_dir = argv[4];
    ensure_dir(out_dir);

    std::ifstream pf(patch_path, std::ios::binary);
    if (!pf) { std::cerr << "Cannot open " << patch_path << std::endl; return 1; }

    // Read header
    char magic[8]; pf.read(magic, 8);
    if (memcmp(magic, GCPATCH_MAGIC, 8) != 0) {
        std::cerr << "Invalid patch file" << std::endl; return 1;
    }
    uint32_t version, flags;
    pf.read(reinterpret_cast<char*>(&version), 4);
    pf.read(reinterpret_cast<char*>(&flags), 4);
    if (version != GCPATCH_VERSION) {
        std::cerr << "Unsupported version " << version << std::endl; return 1;
    }

    // Read string remap
    std::vector<std::pair<uint32_t, uint32_t>> remap_entries;
    {
        uint32_t section_id; pf.read(reinterpret_cast<char*>(&section_id), 4);
        if (section_id != 0xFFFFFFFE) {
            std::cerr << "Expected remap section, got " << section_id << std::endl; return 1;
        }
        uint64_t old_pool_size; pf.read(reinterpret_cast<char*>(&old_pool_size), 8);
        uint32_t count; pf.read(reinterpret_cast<char*>(&count), 4);
        remap_entries.resize(count);
        for (uint32_t i = 0; i < count; i++) {
            pf.read(reinterpret_cast<char*>(&remap_entries[i].first), 4);
            pf.read(reinterpret_cast<char*>(&remap_entries[i].second), 4);
        }
        std::cerr << "Loaded " << count << " string remap entries" << std::endl;
    }

    // Detect struct strides from current files
    auto cur_ways = read_file(cur_dir + "/street_ways.bin");
    size_t way_stride = (cur_ways.size() % 12 == 0 && cur_ways.size() > 0) ? 12 : 9;
    cur_ways.clear();
    auto cur_interps = read_file(cur_dir + "/interp_ways.bin");
    size_t interp_stride = (cur_interps.size() % 20 == 0 && cur_interps.size() > 0) ? 20 : 18;
    cur_interps.clear();
    auto cur_admin = read_file(cur_dir + "/admin_polygons.bin");
    size_t admin_stride = 24;
    for (size_t s : {24, 20, 19}) {
        if (cur_admin.size() % s == 0 && cur_admin.size() > 0) { admin_stride = s; break; }
    }
    cur_admin.clear();

    // Which files need string remap before delta decompression?
    auto needs_remap = [](uint32_t fid) -> bool {
        return fid == (uint32_t)PatchFileId::ADDR_POINTS ||
               fid == (uint32_t)PatchFileId::STREET_WAYS ||
               fid == (uint32_t)PatchFileId::INTERP_WAYS ||
               fid == (uint32_t)PatchFileId::ADMIN_POLYGONS;
    };

    // Process file sections
    while (pf) {
        uint32_t file_id; pf.read(reinterpret_cast<char*>(&file_id), 4);
        if (!pf || file_id == 0xFFFFFFFF) break;

        uint32_t encoding; pf.read(reinterpret_cast<char*>(&encoding), 4);
        uint64_t old_size, new_size, delta_size;
        pf.read(reinterpret_cast<char*>(&old_size), 8);
        pf.read(reinterpret_cast<char*>(&new_size), 8);
        pf.read(reinterpret_cast<char*>(&delta_size), 8);

        if (file_id >= (uint32_t)PatchFileId::COUNT) {
            pf.seekg(delta_size, std::ios::cur);
            continue;
        }
        const char* filename = patch_file_names[file_id];

        // Read delta data
        std::vector<char> delta(delta_size);
        pf.read(delta.data(), delta_size);

        // Load old file
        auto old_data = read_file(cur_dir + "/" + std::string(filename));

        // Apply string remap if needed
        if (needs_remap(file_id)) {
            if (file_id == (uint32_t)PatchFileId::ADDR_POINTS)
                apply_remap_addr_points(old_data, remap_entries);
            else if (file_id == (uint32_t)PatchFileId::STREET_WAYS)
                apply_remap_ways(old_data, way_stride, remap_entries);
            else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS)
                apply_remap_interps(old_data, interp_stride, remap_entries);
            else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS)
                apply_remap_admin(old_data, admin_stride, remap_entries);
        }

        // Decompress delta using (remapped) old file as prefix
        auto new_data = zstd_delta_decompress(old_data.data(), old_data.size(),
                                                delta.data(), delta.size(), new_size);
        if (new_data.empty() && new_size > 0) {
            std::cerr << "FAILED: " << filename << std::endl;
            return 1;
        }

        write_file(out_dir + "/" + std::string(filename), new_data);
        std::cerr << "  " << filename << ": " << new_data.size() << " bytes" << std::endl;
    }

    // Copy manifest.json if it exists
    auto manifest = read_file(cur_dir + "/manifest.json");
    if (!manifest.empty()) {
        // The new build may have a different manifest; for now copy old
        // (a future version could include manifest in the patch)
    }

    std::cerr << "Patch applied. Output in " << out_dir << std::endl;
    return 0;
}


The file /root/traccar-geocoder/builder/tools/geocoder_patch.cpp has been updated successfully.

// geocoder-patch: Apply a .gcpatch to a geocoder index directory.
//
// For each file: reads old file, applies string remap (if applicable),
// then applies zstd delta decompression to produce the new file.
//
// Usage: geocoder-patch <current-dir> <patch-file> -o <output-dir>

#include <algorithm>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

#include <zstd.h>
#include "patch_format.h"

// --- String remap application ---
// Same functions as the diff tool, applied to old files before delta decompression.

static void apply_remap_addr_points(std::vector<char>& data,
                                      const std::vector<std::pair<uint32_t,uint32_t>>& entries) {
    std::unordered_map<uint32_t, uint32_t> rm;
    rm.reserve(entries.size());
    for (auto& [o, n] : entries) rm[o] = n;
    for (size_t i = 0; i + 16 <= data.size(); i += 16) {
        for (size_t off : {8, 12}) {
            uint32_t v; memcpy(&v, data.data() + i + off, 4);
            auto it = rm.find(v);
            if (it != rm.end()) { memcpy(data.data() + i + off, &it->second, 4); }
        }
    }
}

static void apply_remap_ways(std::vector<char>& data, size_t stride,
                               const std::vector<std::pair<uint32_t,uint32_t>>& entries) {
    std::unordered_map<uint32_t, uint32_t> rm;
    rm.reserve(entries.size());
    for (auto& [o, n] : entries) rm[o] = n;
    size_t name_off = (stride == 12) ? 8 : 5;
    for (size_t i = 0; i + stride <= data.size(); i += stride) {
        uint32_t v; memcpy(&v, data.data() + i + name_off, 4);
        auto it = rm.find(v);
        if (it != rm.end()) memcpy(data.data() + i + name_off, &it->second, 4);
    }
}

static void apply_remap_interps(std::vector<char>& data, size_t stride,
                                  const std::vector<std::pair<uint32_t,uint32_t>>& entries) {
    std::unordered_map<uint32_t, uint32_t> rm;
    rm.reserve(entries.size());
    for (auto& [o, n] : entries) rm[o] = n;
    size_t street_off = (stride >= 20) ? 8 : 5;
    for (size_t i = 0; i + stride <= data.size(); i += stride) {
        uint32_t v; memcpy(&v, data.data() + i + street_off, 4);
        auto it = rm.find(v);
        if (it != rm.end()) memcpy(data.data() + i + street_off, &it->second, 4);
    }
}

static void apply_remap_admin(std::vector<char>& data, size_t stride,
                                const std::vector<std::pair<uint32_t,uint32_t>>& entries) {
    std::unordered_map<uint32_t, uint32_t> rm;
    rm.reserve(entries.size());
    for (auto& [o, n] : entries) rm[o] = n;
    for (size_t i = 0; i + stride <= data.size(); i += stride) {
        uint32_t v; memcpy(&v, data.data() + i + 8, 4);
        auto it = rm.find(v);
        if (it != rm.end()) memcpy(data.data() + i + 8, &it->second, 4);
    }
}

// --- zstd delta decompression ---

static std::vector<char> zstd_delta_decompress(const char* prefix_data, size_t prefix_size,
                                                 const char* delta_data, size_t delta_size,
                                                 size_t expected_size) {
    ZSTD_DCtx* dctx = ZSTD_createDCtx();
    ZSTD_DCtx_setParameter(dctx, ZSTD_d_windowLogMax, 31);
    ZSTD_DCtx_refPrefix(dctx, prefix_data, prefix_size);

    std::vector<char> out(expected_size);
    size_t result = ZSTD_decompressDCtx(dctx, out.data(), expected_size, delta_data, delta_size);
    ZSTD_freeDCtx(dctx);

    if (ZSTD_isError(result)) {
        std::cerr << "zstd decompress error: " << ZSTD_getErrorName(result) << std::endl;
        return {};
    }
    out.resize(result);
    return out;
}

int main(int argc, char* argv[]) {
    if (argc < 5 || std::string(argv[3]) != "-o") {
        std::cerr << "Usage: geocoder-patch <current-dir> <patch-file> -o <output-dir>" << std::endl;
        return 1;
    }
    std::string cur_dir = argv[1], patch_path = argv[2], out_dir = argv[4];
    ensure_dir(out_dir);

    std::ifstream pf(patch_path, std::ios::binary);
    if (!pf) { std::cerr << "Cannot open " << patch_path << std::endl; return 1; }

    // Read header
    char magic[8]; pf.read(magic, 8);
    if (memcmp(magic, GCPATCH_MAGIC, 8) != 0) {
        std::cerr << "Invalid patch file" << std::endl; return 1;
    }
    uint32_t version, flags;
    pf.read(reinterpret_cast<char*>(&version), 4);
    pf.read(reinterpret_cast<char*>(&flags), 4);
    if (version != GCPATCH_VERSION) {
        std::cerr << "Unsupported version " << version << std::endl; return 1;
    }

    // Read string remap
    std::vector<std::pair<uint32_t, uint32_t>> remap_entries;
    {
        uint32_t section_id; pf.read(reinterpret_cast<char*>(&section_id), 4);
        if (section_id != 0xFFFFFFFE) {
            std::cerr << "Expected remap section, got " << section_id << std::endl; return 1;
        }
        uint64_t old_pool_size; pf.read(reinterpret_cast<char*>(&old_pool_size), 8);
        uint32_t count; pf.read(reinterpret_cast<char*>(&count), 4);
        remap_entries.resize(count);
        for (uint32_t i = 0; i < count; i++) {
            pf.read(reinterpret_cast<char*>(&remap_entries[i].first), 4);
            pf.read(reinterpret_cast<char*>(&remap_entries[i].second), 4);
        }
        std::cerr << "Loaded " << count << " string remap entries" << std::endl;
    }

    // Detect struct strides from current files
    auto cur_ways = read_file(cur_dir + "/street_ways.bin");
    size_t way_stride = (cur_ways.size() % 12 == 0 && cur_ways.size() > 0) ? 12 : 9;
    cur_ways.clear();
    auto cur_interps = read_file(cur_dir + "/interp_ways.bin");
    size_t interp_stride = (cur_interps.size() % 20 == 0 && cur_interps.size() > 0) ? 20 : 18;
    cur_interps.clear();
    auto cur_admin = read_file(cur_dir + "/admin_polygons.bin");
    size_t admin_stride = 24;
    for (size_t s : {24, 20, 19}) {
        if (cur_admin.size() % s == 0 && cur_admin.size() > 0) { admin_stride = s; break; }
    }
    cur_admin.clear();

    // Which files need string remap before delta decompression?
    auto needs_remap = [](uint32_t fid) -> bool {
        return fid == (uint32_t)PatchFileId::ADDR_POINTS ||
               fid == (uint32_t)PatchFileId::STREET_WAYS ||
               fid == (uint32_t)PatchFileId::INTERP_WAYS ||
               fid == (uint32_t)PatchFileId::ADMIN_POLYGONS;
    };

    // Process file sections
    while (pf) {
        uint32_t file_id; pf.read(reinterpret_cast<char*>(&file_id), 4);
        if (!pf || file_id == 0xFFFFFFFF) break;

        uint32_t encoding; pf.read(reinterpret_cast<char*>(&encoding), 4);
        uint64_t old_size, new_size, delta_size;
        pf.read(reinterpret_cast<char*>(&old_size), 8);
        pf.read(reinterpret_cast<char*>(&new_size), 8);
        pf.read(reinterpret_cast<char*>(&delta_size), 8);

        if (file_id >= (uint32_t)PatchFileId::COUNT) {
            pf.seekg(delta_size, std::ios::cur);
            continue;
        }
        const char* filename = patch_file_names[file_id];

        // Read delta data
        std::vector<char> delta(delta_size);
        pf.read(delta.data(), delta_size);

        // Load old file
        auto old_data = read_file(cur_dir + "/" + std::string(filename));

        // Apply string remap if needed
        if (needs_remap(file_id)) {
            if (file_id == (uint32_t)PatchFileId::ADDR_POINTS)
                apply_remap_addr_points(old_data, remap_entries);
            else if (file_id == (uint32_t)PatchFileId::STREET_WAYS)
                apply_remap_ways(old_data, way_stride, remap_entries);
            else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS)
                apply_remap_interps(old_data, interp_stride, remap_entries);
            else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS)
                apply_remap_admin(old_data, admin_stride, remap_entries);
        }

        // Decompress delta using (remapped) old file as prefix
        auto new_data = zstd_delta_decompress(old_data.data(), old_data.size(),
                                                delta.data(), delta.size(), new_size);
        if (new_data.empty() && new_size > 0) {
            std::cerr << "FAILED: " << filename << std::endl;
            return 1;
        }

        write_file(out_dir + "/" + std::string(filename), new_data);
        std::cerr << "  " << filename << ": " << new_data.size() << " bytes" << std::endl;
    }

    // Copy manifest.json if it exists
    auto manifest = read_file(cur_dir + "/manifest.json");
    if (!manifest.empty()) {
        // The new build may have a different manifest; for now copy old
        // (a future version could include manifest in the patch)
    }

    std::cerr << "Patch applied. Output in " << out_dir << std::endl;
    return 0;
}

-// geocoder-patch: Apply a .gcpatch file to a canonical geocoder index.

+// geocoder-patch: Apply a .gcpatch to a geocoder index directory.

+// For each file: reads old file, applies string remap (if applicable),

+// then applies zstd delta decompression to produce the new file.

-static uint32_t read_u32(std::ifstream& f) {

-    uint32_t v; f.read(reinterpret_cast<char*>(&v), 4); return v;

+// Same functions as the diff tool, applied to old files before delta decompression.

+static void apply_remap_addr_points(std::vector<char>& data,

+                                      const std::vector<std::pair<uint32_t,uint32_t>>& entries) {

+    std::unordered_map<uint32_t, uint32_t> rm;

+    for (auto& [o, n] : entries) rm[o] = n;

+            uint32_t v; memcpy(&v, data.data() + i + off, 4);

+            if (it != rm.end()) { memcpy(data.data() + i + off, &it->second, 4); }

-static uint64_t read_u64(std::ifstream& f) {

-    uint64_t v; f.read(reinterpret_cast<char*>(&v), 8); return v;

+static void apply_remap_ways(std::vector<char>& data, size_t stride,

+                               const std::vector<std::pair<uint32_t,uint32_t>>& entries) {

+    size_t name_off = (stride == 12) ? 8 : 5;

+    for (size_t i = 0; i + stride <= data.size(); i += stride) {

+        uint32_t v; memcpy(&v, data.data() + i + name_off, 4);

+        if (it != rm.end()) memcpy(data.data() + i + name_off, &it->second, 4);

-static std::vector<char> zlib_decompress(const char* data, size_t compressed_size, size_t uncompressed_size) {

-    std::vector<char> out(uncompressed_size);

-    uLongf dest_len = uncompressed_size;

-    int ret = uncompress(reinterpret_cast<Bytef*>(out.data()), &dest_len,

-                         reinterpret_cast<const Bytef*>(data), compressed_size);

-        std::cerr << "zlib decompress failed: " << ret << std::endl;

+static void apply_remap_interps(std::vector<char>& data, size_t stride,

+                                  const std::vector<std::pair<uint32_t,uint32_t>>& entries) {

+    std::unordered_map<uint32_t, uint32_t> rm;

+    for (auto& [o, n] : entries) rm[o] = n;

+    size_t street_off = (stride >= 20) ? 8 : 5;

+        uint32_t v; memcpy(&v, data.data() + i + street_off, 4);

+        if (it != rm.end()) memcpy(data.data() + i + street_off, &it->second, 4);

+static void apply_remap_admin(std::vector<char>& data, size_t stride,

+                                const std::vector<std::pair<uint32_t,uint32_t>>& entries) {

+        uint32_t v; memcpy(&v, data.data() + i + 8, 4);

+        if (it != rm.end()) memcpy(data.data() + i + 8, &it->second, 4);

+static std::vector<char> zstd_delta_decompress(const char* prefix_data, size_t prefix_size,

+                                                 const char* delta_data, size_t delta_size,

+                                                 size_t expected_size) {

+    ZSTD_DCtx* dctx = ZSTD_createDCtx();

+    ZSTD_DCtx_setParameter(dctx, ZSTD_d_windowLogMax, 31);

+    ZSTD_DCtx_refPrefix(dctx, prefix_data, prefix_size);

+    std::vector<char> out(expected_size);

+    size_t result = ZSTD_decompressDCtx(dctx, out.data(), expected_size, delta_data, delta_size);

+        std::cerr << "zstd decompress error: " << ZSTD_getErrorName(result) << std::endl;

+    std::string cur_dir = argv[1], patch_path = argv[2], out_dir = argv[4];

if (memcmp(magic, GCPATCH_MAGIC, 8) != 0) {

-        std::cerr << "Invalid patch file (bad magic)" << std::endl;

+        std::cerr << "Invalid patch file" << std::endl; return 1;

+    pf.read(reinterpret_cast<char*>(&version), 4);

+    pf.read(reinterpret_cast<char*>(&flags), 4);

-        std::cerr << "Unsupported patch version: " << version << std::endl;

+        std::cerr << "Unsupported version " << version << std::endl; return 1;

-    std::cerr << "Applying patch from " << patch_path << "..." << std::endl;

-        uint32_t section_id = read_u32(pf);

-        if (!pf || section_id == 0xFFFFFFFF) break; // end marker

-        // String remap section (special)

-        if (section_id == 0xFFFFFFFE) {

-            uint32_t count = read_u32(pf);

-            // Skip remap entries (not needed for RAW_REPLACE mode)

-            pf.seekg(count * 8, std::ios::cur);

-            std::cerr << "  Skipping string remap (" << count << " entries)" << std::endl;

+    std::vector<std::pair<uint32_t, uint32_t>> remap_entries;

+        uint32_t section_id; pf.read(reinterpret_cast<char*>(&section_id), 4);

+        if (section_id != 0xFFFFFFFE) {

+            std::cerr << "Expected remap section, got " << section_id << std::endl; return 1;

-        uint32_t encoding = read_u32(pf);

-        if (section_id >= static_cast<uint32_t>(PatchFileId::COUNT)) {

-            std::cerr << "  Unknown section " << section_id << std::endl;

-            // Can't skip unknown sections reliably, abort

+        uint64_t old_pool_size; pf.read(reinterpret_cast<char*>(&old_pool_size), 8);

+        uint32_t count; pf.read(reinterpret_cast<char*>(&count), 4);

+        for (uint32_t i = 0; i < count; i++) {

+            pf.read(reinterpret_cast<char*>(&remap_entries[i].first), 4);

+            pf.read(reinterpret_cast<char*>(&remap_entries[i].second), 4);

+        std::cerr << "Loaded " << count << " string remap entries" << std::endl;

-        const char* filename = patch_file_names[section_id];

+    // Detect struct strides from current files

+    auto cur_ways = read_file(cur_dir + "/street_ways.bin");

+    size_t way_stride = (cur_ways.size() % 12 == 0 && cur_ways.size() > 0) ? 12 : 9;

+    auto cur_interps = read_file(cur_dir + "/interp_ways.bin");

+    size_t interp_stride = (cur_interps.size() % 20 == 0 && cur_interps.size() > 0) ? 20 : 18;

+    auto cur_admin = read_file(cur_dir + "/admin_polygons.bin");

+        if (cur_admin.size() % s == 0 && cur_admin.size() > 0) { admin_stride = s; break; }

-        if (encoding == static_cast<uint32_t>(PatchEncoding::RAW_REPLACE)) {

-            uint64_t uncompressed_size = read_u64(pf);

-            uint64_t compressed_size = read_u64(pf);

-            std::vector<char> compressed(compressed_size);

-            pf.read(compressed.data(), compressed_size);

-            auto data = zlib_decompress(compressed.data(), compressed_size, uncompressed_size);

-            if (data.empty() && uncompressed_size > 0) {

-                std::cerr << "  FAILED to decompress " << filename << std::endl;

-            write_file(output_dir + "/" + filename, data);

-            std::cerr << "  " << filename << ": replaced (" << data.size() << " bytes)" << std::endl;

+    // Which files need string remap before delta decompression?

+    auto needs_remap = [](uint32_t fid) -> bool {

+        return fid == (uint32_t)PatchFileId::ADDR_POINTS ||

+               fid == (uint32_t)PatchFileId::STREET_WAYS ||

+               fid == (uint32_t)PatchFileId::INTERP_WAYS ||

+               fid == (uint32_t)PatchFileId::ADMIN_POLYGONS;

-        } else if (encoding == static_cast<uint32_t>(PatchEncoding::COPY_INSERT)) {

-            uint64_t old_size = read_u64(pf);

-            uint64_t new_size = read_u64(pf);

-            uint64_t compressed_size_local = read_u64(pf);

+        uint32_t file_id; pf.read(reinterpret_cast<char*>(&file_id), 4);

+        if (!pf || file_id == 0xFFFFFFFF) break;

-            std::vector<char> compressed(compressed_size_local);

-            pf.read(compressed.data(), compressed_size_local);

+        uint32_t encoding; pf.read(reinterpret_cast<char*>(&encoding), 4);

+        uint64_t old_size, new_size, delta_size;

+        pf.read(reinterpret_cast<char*>(&old_size), 8);

+        pf.read(reinterpret_cast<char*>(&new_size), 8);

+        pf.read(reinterpret_cast<char*>(&delta_size), 8);

-            // Estimate ops uncompressed size (generous: 4x compressed)

-            size_t est_size = compressed_size_local * 8;

-            if (est_size < new_size * 2) est_size = new_size * 2;

-            auto ops = zlib_decompress(compressed.data(), compressed_size_local, est_size);

-            if (ops.empty() && compressed_size_local > 0) {

-                std::cerr << "  FAILED to decompress ops for " << filename << std::endl;

+        if (file_id >= (uint32_t)PatchFileId::COUNT) {

+            pf.seekg(delta_size, std::ios::cur);

+        const char* filename = patch_file_names[file_id];

-            auto old_data = read_file(current_dir + "/" + filename);

-            if (old_data.size() != old_size) {

-                std::cerr << "  WARNING: " << filename << " size mismatch (expected "

-                          << old_size << ", got " << old_data.size() << ")" << std::endl;

+        std::vector<char> delta(delta_size);

+        pf.read(delta.data(), delta_size);

-            std::vector<char> new_data;

-            new_data.reserve(new_size);

-                uint8_t tag = static_cast<uint8_t>(ops[pos]); pos++;

-                    uint64_t off; memcpy(&off, ops.data() + pos, 8); pos += 8;

-                    uint32_t len; memcpy(&len, ops.data() + pos, 4); pos += 4;

-                    if (off + len <= old_data.size()) {

-                        new_data.insert(new_data.end(), old_data.data() + off,

-                                       old_data.data() + off + len);

-                } else if (tag == OP_INSERT) {

-                    new_data.insert(new_data.end(), ops.data() + pos, ops.data() + pos + len);

-                    std::cerr << "  Unknown op " << (int)tag << " in " << filename << std::endl;

+        auto old_data = read_file(cur_dir + "/" + std::string(filename));

-            write_file(output_dir + "/" + filename, new_data);

-            std::cerr << "  " << filename << ": delta applied (" << new_data.size() << " bytes)" << std::endl;

+        // Apply string remap if needed

+            if (file_id == (uint32_t)PatchFileId::ADDR_POINTS)

+                apply_remap_addr_points(old_data, remap_entries);

+            else if (file_id == (uint32_t)PatchFileId::STREET_WAYS)

+                apply_remap_ways(old_data, way_stride, remap_entries);

+            else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS)

+                apply_remap_interps(old_data, interp_stride, remap_entries);

+            else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS)

+                apply_remap_admin(old_data, admin_stride, remap_entries);

-            std::cerr << "  Unknown encoding " << encoding << " for " << filename << std::endl;

+        // Decompress delta using (remapped) old file as prefix

+        auto new_data = zstd_delta_decompress(old_data.data(), old_data.size(),

+                                                delta.data(), delta.size(), new_size);

+        if (new_data.empty() && new_size > 0) {

+            std::cerr << "FAILED: " << filename << std::endl;

+        write_file(out_dir + "/" + std::string(filename), new_data);

+        std::cerr << "  " << filename << ": " << new_data.size() << " bytes" << std::endl;

-    std::cerr << "Patch applied. Output in " << output_dir << std::endl;

+    auto manifest = read_file(cur_dir + "/manifest.json");

+        // The new build may have a different manifest; for now copy old

+        // (a future version could include manifest in the patch)

+    std::cerr << "Patch applied. Output in " << out_dir << std::endl;

// geocoder-patch: Apply a .gcpatch file to a canonical geocoder index.
//
// Usage: geocoder-patch <current-dir> <patch-file> -o <output-dir>

#include <algorithm>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include <zlib.h>

#include "patch_format.h"

static uint32_t read_u32(std::ifstream& f) {
    uint32_t v; f.read(reinterpret_cast<char*>(&v), 4); return v;
}
static uint64_t read_u64(std::ifstream& f) {
    uint64_t v; f.read(reinterpret_cast<char*>(&v), 8); return v;
}

static std::vector<char> zlib_decompress(const char* data, size_t compressed_size, size_t uncompressed_size) {
    std::vector<char> out(uncompressed_size);
    uLongf dest_len = uncompressed_size;
    int ret = uncompress(reinterpret_cast<Bytef*>(out.data()), &dest_len,
                         reinterpret_cast<const Bytef*>(data), compressed_size);
    if (ret != Z_OK) {
        std::cerr << "zlib decompress failed: " << ret << std::endl;
        return {};
    }
    out.resize(dest_len);
    return out;
}

int main(int argc, char* argv[]) {
    if (argc < 5 || std::string(argv[3]) != "-o") {
        std::cerr << "Usage: geocoder-patch <current-dir> <patch-file> -o <output-dir>" << std::endl;
        return 1;
    }

    std::string current_dir = argv[1];
    std::string patch_path = argv[2];
    std::string output_dir = argv[4];
    ensure_dir(output_dir);

    // Open patch file
    std::ifstream pf(patch_path, std::ios::binary);
    if (!pf) { std::cerr << "Cannot open " << patch_path << std::endl; return 1; }

    // Read and verify header
    char magic[8];
    pf.read(magic, 8);
    if (memcmp(magic, GCPATCH_MAGIC, 8) != 0) {
        std::cerr << "Invalid patch file (bad magic)" << std::endl;
        return 1;
    }
    uint32_t version = read_u32(pf);
    if (version != GCPATCH_VERSION) {
        std::cerr << "Unsupported patch version: " << version << std::endl;
        return 1;
    }
    uint32_t flags = read_u32(pf);
    (void)flags;

    std::cerr << "Applying patch from " << patch_path << "..." << std::endl;

    // Process sections
    while (pf) {
        uint32_t section_id = read_u32(pf);
        if (!pf || section_id == 0xFFFFFFFF) break; // end marker

        // String remap section (special)
        if (section_id == 0xFFFFFFFE) {
            uint32_t count = read_u32(pf);
            // Skip remap entries (not needed for RAW_REPLACE mode)
            pf.seekg(count * 8, std::ios::cur);
            std::cerr << "  Skipping string remap (" << count << " entries)" << std::endl;
            continue;
        }

        uint32_t encoding = read_u32(pf);

        if (section_id >= static_cast<uint32_t>(PatchFileId::COUNT)) {
            std::cerr << "  Unknown section " << section_id << std::endl;
            // Can't skip unknown sections reliably, abort
            break;
        }

        const char* filename = patch_file_names[section_id];

        if (encoding == static_cast<uint32_t>(PatchEncoding::RAW_REPLACE)) {
            uint64_t uncompressed_size = read_u64(pf);
            uint64_t compressed_size = read_u64(pf);
            std::vector<char> compressed(compressed_size);
            pf.read(compressed.data(), compressed_size);
            auto data = zlib_decompress(compressed.data(), compressed_size, uncompressed_size);
            if (data.empty() && uncompressed_size > 0) {
                std::cerr << "  FAILED to decompress " << filename << std::endl;
                return 1;
            }
            write_file(output_dir + "/" + filename, data);
            std::cerr << "  " << filename << ": replaced (" << data.size() << " bytes)" << std::endl;

        } else if (encoding == static_cast<uint32_t>(PatchEncoding::COPY_INSERT)) {
            uint64_t old_size = read_u64(pf);
            uint64_t new_size = read_u64(pf);
            uint64_t compressed_size_local = read_u64(pf);

            // Read and decompress ops
            std::vector<char> compressed(compressed_size_local);
            pf.read(compressed.data(), compressed_size_local);

            // Estimate ops uncompressed size (generous: 4x compressed)
            size_t est_size = compressed_size_local * 8;
            if (est_size < new_size * 2) est_size = new_size * 2;
            auto ops = zlib_decompress(compressed.data(), compressed_size_local, est_size);
            if (ops.empty() && compressed_size_local > 0) {
                std::cerr << "  FAILED to decompress ops for " << filename << std::endl;
                return 1;
            }

            // Load old file
            auto old_data = read_file(current_dir + "/" + filename);
            if (old_data.size() != old_size) {
                std::cerr << "  WARNING: " << filename << " size mismatch (expected "
                          << old_size << ", got " << old_data.size() << ")" << std::endl;
            }

            // Replay COPY/INSERT ops
            std::vector<char> new_data;
            new_data.reserve(new_size);
            size_t pos = 0;
            while (pos < ops.size()) {
                uint8_t tag = static_cast<uint8_t>(ops[pos]); pos++;
                if (tag == OP_COPY) {
                    uint64_t off; memcpy(&off, ops.data() + pos, 8); pos += 8;
                    uint32_t len; memcpy(&len, ops.data() + pos, 4); pos += 4;
                    if (off + len <= old_data.size()) {
                        new_data.insert(new_data.end(), old_data.data() + off,
                                       old_data.data() + off + len);
                    }
                } else if (tag == OP_INSERT) {
                    uint32_t len; memcpy(&len, ops.data() + pos, 4); pos += 4;
                    new_data.insert(new_data.end(), ops.data() + pos, ops.data() + pos + len);
                    pos += len;
                } else {
                    std::cerr << "  Unknown op " << (int)tag << " in " << filename << std::endl;
                    break;
                }
            }

            write_file(output_dir + "/" + filename, new_data);
            std::cerr << "  " << filename << ": delta applied (" << new_data.size() << " bytes)" << std::endl;

        } else {
            std::cerr << "  Unknown encoding " << encoding << " for " << filename << std::endl;
        }
    }

    std::cerr << "Patch applied. Output in " << output_dir << std::endl;
    return 0;
}


Update CMakeLists.txt to link against zstd:


Edit

add_executable(geocoder-diff tools/geocoder_diff.cpp)
target_link_libraries(geocoder-diff PRIVATE ZLIB::ZLIB Threads::Threads)
target_include_directories(geocoder-diff PRIVATE tools)

add_executable(geocoder-patch tools/geocoder_patch.cpp)
target_link_libraries(geocoder-patch PRIVATE ZLIB::ZLIB Threads::Threads)
target_include_directories(geocoder-patch PRIVATE tools)

add_executable(geocoder-diff tools/geocoder_diff.cpp)
target_link_libraries(geocoder-diff PRIVATE zstd Threads::Threads)
target_include_directories(geocoder-diff PRIVATE tools)

add_executable(geocoder-patch tools/geocoder_patch.cpp)
target_link_libraries(geocoder-patch PRIVATE zstd Threads::Threads)
target_include_directories(geocoder-patch PRIVATE tools)


The file /root/traccar-geocoder/builder/CMakeLists.txt has been updated successfully.

add_executable(geocoder-diff tools/geocoder_diff.cpp)
target_link_libraries(geocoder-diff PRIVATE zstd Threads::Threads)
target_include_directories(geocoder-diff PRIVATE tools)

add_executable(geocoder-patch tools/geocoder_patch.cpp)
target_link_libraries(geocoder-patch PRIVATE zstd Threads::Threads)
target_include_directories(geocoder-patch PRIVATE tools)

cmake_minimum_required(VERSION 3.16)
project(traccar-geocoder-build LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Threads REQUIRED)
find_package(ZLIB REQUIRED)
find_package(BZip2 REQUIRED)
find_package(EXPAT REQUIRED)
find_package(s2 QUIET)

add_executable(build-index
    src/build_index.cpp
    src/pbf_reader.cpp
    src/s2_helpers.cpp
    src/ring_assembly.cpp
    src/cache.cpp
    src/continent_filter.cpp
    src/continent_boundaries.cpp
    src/cell_index.cpp
    src/interpolation.cpp
)

# Patch tools (no S2 dependency)
add_executable(geocoder-canonicalize tools/geocoder_canonicalize.cpp)
target_link_libraries(geocoder-canonicalize PRIVATE Threads::Threads)
target_include_directories(geocoder-canonicalize PRIVATE tools)

add_executable(geocoder-diff tools/geocoder_diff.cpp)
target_link_libraries(geocoder-diff PRIVATE ZLIB::ZLIB Threads::Threads)
target_include_directories(geocoder-diff PRIVATE tools)

add_executable(geocoder-patch tools/geocoder_patch.cpp)
target_link_libraries(geocoder-patch PRIVATE ZLIB::ZLIB Threads::Threads)
target_include_directories(geocoder-patch PRIVATE tools)

add_executable(remap-test tools/remap_test.cpp)
target_link_libraries(remap-test PRIVATE Threads::Threads)
target_include_directories(remap-test PRIVATE tools)

add_executable(full-remap-test tools/full_remap_test.cpp)
target_link_libraries(full-remap-test PRIVATE Threads::Threads)
target_include_directories(full-remap-test PRIVATE tools)

if(EXISTS /opt/homebrew/include)
    target_include_directories(build-index PRIVATE /opt/homebrew/include)
endif()

if(s2_FOUND)
    target_link_libraries(build-index PRIVATE
        s2::s2
        ZLIB::ZLIB
        BZip2::BZip2
        EXPAT::EXPAT
        Threads::Threads
    )
else()
    target_include_directories(build-index PRIVATE /usr/include/s2)
    target_link_libraries(build-index PRIVATE
        s2
        ZLIB::ZLIB
        BZip2::BZip2
        EXPAT::EXPAT
        Threads::Threads
    )
endif()

-target_link_libraries(geocoder-diff PRIVATE ZLIB::ZLIB Threads::Threads)

+target_link_libraries(geocoder-diff PRIVATE zstd Threads::Threads)

-target_link_libraries(geocoder-patch PRIVATE ZLIB::ZLIB Threads::Threads)

+target_link_libraries(geocoder-patch PRIVATE zstd Threads::Threads)


_cfuvid=6oXGAr1fRzDyN8XcRDmWXXBc0gzhNpinsLQfXadCb6c-1774607649.277372-1.0.1.1-sH.CPlPeP7VxSnOIGgp9sWnzsepVwTI3c6Mv7L0E_xc; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com


Bash

cmake -B build . 2>&1 | tail -3 && cmake --build build -j$(nproc) --target geocoder-diff --target geocoder-patch 2>&1 | tail -10

Build diff and patch tools with zstd


-- Configuring done (0.0s)
-- Generating done (0.0s)
-- Build files have been written to: /root/traccar-geocoder/builder/build
  215 |         std::cerr << delta.size() << " bytes (" << std::fixed << std::setprecision(1) << pct
      |                                                                       ^~~~~~~~~~~~
/root/traccar-geocoder/builder/tools/geocoder_diff.cpp:26:1: note: 'std::setprecision' is defined in header '<iomanip>'; this is probably fixable by adding '#include <iomanip>'
   25 | #include "patch_format.h"
  +++ |+#include <iomanip>
   26 |
gmake[3]: *** [CMakeFiles/geocoder-diff.dir/build.make:79: CMakeFiles/geocoder-diff.dir/tools/geocoder_diff.cpp.o] Error 1
gmake[2]: *** [CMakeFiles/Makefile2:166: CMakeFiles/geocoder-diff.dir/all] Error 2
gmake[1]: *** [CMakeFiles/Makefile2:173: CMakeFiles/geocoder-diff.dir/rule] Error 2
gmake: *** [Makefile:150: geocoder-diff] Error 2


Edit

#include <algorithm>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <functional>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

#include <zstd.h>

#include <algorithm>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <functional>
#include <iomanip>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

#include <zstd.h>