Rust (tzf-rs)
Install
cargo add tzf-rstzf-rs 2.0 is protobuf-free. It reads the TZF embedded binary format (.tzb)
published by ringsaturn/tzf-dist;
the default bundled feature carries the lite file (~4 MB) inside the crate.
See Embedded Binary Format
for the format itself.
Two finders
| Type | Data | Peak RSS | Query (random city / edge city) |
|---|---|---|---|
DefaultFinder | lite .tzb expanded into polygons, FUZZY fast path | ~47 MiB | 221 ns / 475 ns |
EmbeddedFinder | lite .tzb queried in place | ~10 MiB | 293 ns / 666 ns |
Measured in the tz-benchmark
2026-09-14 snapshot on an Apple M3 Max against the 2026c dataset, tzf-rs 2.1.1
on tzf-dist 0.0.2026-c-tzb2; the Rust runtime floor in that harness is 5.8 MiB.
tzf-rs 2.1 rewrote the EmbeddedFinder query walk (open-time validation, chunk
block and endpoint-parity skipping, per-group latitude stripes, preindex probes
limited to the zoom levels that carry keys) and moved to 64-point-chunk data;
results are unchanged, and the edge-city figure was 4.78 µs on 2.0.0.
DefaultFinder::new() opens in about 13 ms, EmbeddedFinder::new() in about
2 ms. The counting allocator reports 0.2 MiB of heap retention forEmbeddedFinder: its data is the &'static embedded slice, plus the open-time
index of chunk skip blocks and latitude stripes (about 100 KB on lite).EmbeddedFinder applies to memory-constrained targets; its border-city lookups
take about 1.4x the DefaultFinder time. Choosing a Finder
covers the remaining cases.
Finder reuse
Construction loads and validates the whole file, so a process builds one finder
and reuses it. A LazyLock static holds it without an extra dependency:
use std::sync::LazyLock;
use tzf_rs::DefaultFinder;
static FINDER: LazyLock<DefaultFinder> = LazyLock::new(DefaultFinder::new);
fn main() {
// Coordinates are in (longitude, latitude) order.
println!("{:?}", FINDER.get_tz_name(116.3883, 39.9289));
println!("{:?}", FINDER.get_tz_names(116.3883, 39.9289));
}lazy_static also works. LazyLock has been in the standard library since Rust
1.80.
Queries
Both finders expose the same four methods:
finder.get_tz_name(lng, lat) // -> &str, empty when nothing matches
finder.get_tz_names(lng, lat) // -> Vec<&str>, sorted lexicographically
finder.timezonenames() // -> Vec<&str>
finder.data_version() // -> &str, e.g. "2026c"
get_tz_nameis fuzzy-first: the FUZZY tile preindex answers most queries with no point-in-polygon work.get_tz_namesis polygon-exact in both finders and does not consult the preindex. It applies when a point may belong to more than one timezone; a point on a shared border belongs to every touching polygon.
Bring your own bytes
use tzf_rs::{DefaultFinder, EmbeddedFinder};
let data = std::fs::read("lite.tzb")?;
let finder = DefaultFinder::from_tzb(&data)?; // borrows during load, then expands
static DATA: &[u8] = include_bytes!("../data/lite.tzb");
let finder = EmbeddedFinder::from_tzb(DATA)?; // no copy; queries read in place
EmbeddedFinder::from_tzb takes impl Into<Cow<'static, [u8]>>, so a&'static [u8] from include_bytes! is adopted without copying, and an ownedVec<u8> is also accepted. Both constructors return Result<_, tzf_rs::Error>:
files are CRC-checked and structurally validated at open, and malformed data
surfaces as an error rather than an empty finder.
Error is #[non_exhaustive] with Malformed, Profile, NoFuzzy, andIndex variants. .tzm memory-image bytes return Profile: tzf-rs consumes
the .tzb profile only, and the M profile is read by the Go implementation.
Cargo features
| Feature | Default | Effect |
|---|---|---|
bundled | yes | Embeds the lite .tzb from tzf-dist (~4 MB); enables new() |
clap | yes | Builds the tzf CLI binary |
full | no | Git-only full-precision .tzb (~14 MB); enables new_full() |
export-geojson | no | GeoJSON export methods |
full is mutually exclusive with bundled; the crate raises a compile_error!
when both are enabled. The full dataset exceeds the crates.io size limit, so it
is referenced from git:
[dependencies]
tzf-rs = { git = "https://github.com/ringsaturn/tzf-rs", rev = "v{X}.{Y}.{Z}", features = ["full"], default-features = false }use tzf_rs::DefaultFinder;
let finder = DefaultFinder::new_full();
println!("{}", finder.get_tz_name(139.767125, 35.681236));To drop the CLI binary from a library build:cargo build --no-default-features --features bundled.
GeoJSON export
With export-geojson enabled, both finders expose four exporters:
let world = finder.to_geojson(); // BoundaryFile
let tokyo = finder.get_tz_geojson("Asia/Tokyo"); // Option<BoundaryFile>
let tiles = finder.get_tz_preindex_geojson("Asia/Tokyo"); // Option<BoundaryFile>
let all_tiles = finder.to_preindex_geojson(); // Option<BoundaryFile>
The preindex exporters return the bounding rectangles of the FUZZY tiles that
name a timezone: the area where get_tz_name answers from the preindex without
falling back to point-in-polygon. They return None when the file has no FUZZY
section.
Migrating from v1
| v1 | v2 |
|---|---|
DefaultFinder::new() | DefaultFinder::new() — unchanged call sites |
DefaultFinder::new_full() | DefaultFinder::new_full() — unchanged call sites |
Finder (polygon-only) | DefaultFinder; get_tz_names stays polygon-exact |
FuzzyFinder (tile-only) | removed — the preindex is the fast path inside every finder |
Finder::from_compressed_topo(pb) | DefaultFinder::from_tzb(bytes) |
FuzzyFinder::from_pb(pb) | removed, no replacement |
FinderOptions / new_with_options | removed — YStripes is always on |
finder.finder.get_tz_geojson(...) | finder.get_tz_geojson(...) |
FuzzyFinder tile-bbox GeoJSON | replaced by get_tz_preindex_geojson / to_preindex_geojson |
| — | new: EmbeddedFinder, in-place and low-memory |
Behaviour changes:
get_tz_namesresults are now sorted lexicographically.get_tz_nameonDefaultFinderanswers from a covering preindex tile, which matches v1DefaultFindersemantics. Code that used v1Finderfor polygon-exact multi-results callsget_tz_names.- Byte constructors return
Resultinstead of falling back to an empty finder. - GeoJSON exports omit the duplicated junction vertices the protobuf expansion retained (zero-length segments). Query results are unaffected.
- Removed along with protobuf:
Finder::from_pb,Finder::from_compressed_topo,FuzzyFinder::from_pb, thepbgenmodule, theprostdependency, andrevert_timezones.
The v1 line (tzf-rs 1.3.x) remains available and is frozen at its last data release: tzf-dist stops publishing the protobuf artifacts once the v2 set ships, so updated boundaries require moving to v2.
Integration examples
- HTTP service:
racemap/rust-tz-servicewraps tzf-rs in an Axum web server. - Redis protocol:
ringsaturn/redizoneis a Redis-compatible server built on tzf-rs. - PostgreSQL:
ringsaturn/pg-tzfexposes tzf-rs as a database extension.