
Version 2 is protobuf-free. The data source is the TZF embedded binary
format (.tzb) shipped by
tzf-dist, and the public surface
is two finder types: DefaultFinder and EmbeddedFinder. See
Migrating from v1.
Quick start
cargo add tzf-rs
use tzf_rs::DefaultFinder;
fn main() {
let finder = DefaultFinder::new();
// Please note coords are lng-lat.
println!("{:?}", finder.get_tz_name(116.3883, 39.9289));
println!("{:?}", finder.get_tz_names(116.3883, 39.9289));
}
By default the tzf CLI binary is built as well. If you don’t want/need it,
you can omit the default features and build like this:
cargo build --no-default-features --features bundled
Finders
DefaultFinder — the recommended general-purpose finder. Expands the
file’s geometry into materialized polygons at load time. get_tz_name
answers most queries from the preindex tiles in ~100 ns and falls back to
exact point-in-polygon (YStripes-accelerated ray casting) for boundary
cases; get_tz_names is always polygon-exact.
EmbeddedFinder — the low-memory finder. Queries the .tzb bytes in
place, without expanding geometry: total footprint is roughly the ~4 MB
file plus a small open-time index (chunk skip blocks and per-group latitude
stripes, ~100 KB on lite). Boundary queries take a few microseconds instead
of hundreds of nanoseconds; results are identical to DefaultFinder.
Both also load caller-supplied bytes: DefaultFinder::from_tzb /
EmbeddedFinder::from_tzb (EmbeddedFinder takes &'static [u8] or an owned
Vec<u8>). Both expose get_tz_name, get_tz_names, timezonenames and
data_version (the upstream release the file was built from, e.g. 2026c),
and — with export-geojson — to_geojson, get_tz_geojson,
to_preindex_geojson and get_tz_preindex_geojson.
Cargo features
| Feature |
Default |
Effect |
bundled |
yes |
Embeds the lite .tzb (~4 MB) from the crates.io tzf-dist package. |
clap |
yes |
Builds the tzf CLI binary. |
export-geojson |
no |
GeoJSON export methods (pulls in serde / serde_json). |
full |
no |
Full-precision .tzb (~14 MB) plus DefaultFinder::new_full(). The data is git-only; see Setup 100% Accurate Lookup. Mutually exclusive with bundled. |
Minimum supported Rust version: 1.88 (edition 2024 plus let-chains).
There is no wasm-specific feature. wasm32-unknown-unknown builds with
--no-default-features --features bundled (drop clap). Load-time polygon
assembly is distributed over std::thread::available_parallelism(), which
returns an error on wasm; the loader then takes the single-threaded path, so no
threads are spawned there.
Best Practices
It’s expensive to init tzf-rs’s DefaultFinder/EmbeddedFinder, so please
consider reusing instances or creating one as a global variable:
use std::sync::LazyLock;
use tzf_rs::DefaultFinder;
static FINDER: LazyLock<DefaultFinder> = LazyLock::new(DefaultFinder::new);
fn main() {
// Please note coords are lng-lat.
println!("{:?}", FINDER.get_tz_name(116.3883, 39.9289));
println!("{:?}", FINDER.get_tz_names(116.3883, 39.9289));
}
For reuse,
racemap/rust-tz-service provides
a good example.
A Redis protocol demo could be used here:
ringsaturn/redizone.
Setup 100% Accurate Lookup
By default, tzf-rs uses simplified shape data. The error around borders is
small and bounded: every simplified boundary stays within ~111 m of the
full-precision border. See Accuracy for measured numbers. If you
need 100% accurate lookup, use the full-precision dataset (~14 MB).
full.tzb exceeds the crates.io package size limit, so the tzf-dist package
published to the registry ships only lite.tzb. The full feature therefore
needs the tzf-dist git source. There are two ways to get it.
A. Take tzf-rs itself from git (the v1 recipe, still supported):
tzf-rs = { git = "https://github.com/ringsaturn/tzf-rs", rev = "v{X}.{Y}.{Z}", features = ["full"], default-features = false }
B. Keep tzf-rs from crates.io and patch the data crate: add a
[patch.crates-io] section to the workspace root manifest.
[dependencies]
tzf-rs = { version = "2", features = ["full"], default-features = false }
[patch.crates-io]
tzf-dist = { git = "https://github.com/ringsaturn/tzf-dist", tag = "v0.0.2026-c-tzb1" }
Either way default-features = false is required: full and the default
bundled feature are mutually exclusive, and enabling both is a
compile_error!. Without one of these two recipes, features = ["full"] fails
to compile, because the registry tzf-dist package contains no full dataset.
use tzf_rs::DefaultFinder;
fn main() {
let finder = DefaultFinder::new_full();
println!("{}", finder.timezonenames().len());
let tz_name = finder.get_tz_name(139.767125, 35.681236);
println!("tz_name: {}", tz_name);
}
This setup requires more time and memory to build the DefaultFinder.
Advanced Usage - Export GeoJSON
This feature is designed for data visualization purposes and I can’t
guarantee the performance when using it in high-performance scenarios. Please
do proper performance tests and necessary optimizations before using it in
high performace production, for example caching the exported GeoJSON data or
push to CDN.
It’s a common use case make some visualization of timezone boundaries. For this
purpose, tzf-rs provides methods to export specific timezone polygons as
GeoJSON format.
To enable this feature, you need to build tzf-rs with export-geojson feature:
tzf-rs = { version = "{version}", features = ["export-geojson"]}
Then you can use the following methods:
// examples/query_tokyo.rs
use tzf_rs::DefaultFinder;
fn main() {
let default_finder = DefaultFinder::new();
let lng = 139.6917;
let lat = 35.6895;
let tz_name = default_finder.get_tz_name(lng, lat).to_owned();
println!(
"The timezone at longitude {}, latitude {} is: {}",
lng, lat, tz_name
);
// Get the polygon boundary for the timezone.
if let Some(boundary_file) = default_finder.get_tz_geojson(&tz_name) {
// It's a GeoJSON FeatureCollection whose features contain
// "MultiPolygon" geometry for the timezone.
println!("Found GeoJSON feature for timezone: {}", tz_name);
let mut polygons: usize = 0;
for feature in boundary_file.features {
polygons += feature.geometry.coordinates.len();
}
println!(
"Total number of polygons in feature collection: {}",
polygons
);
}
}
cargo run --example query_tokyo --features export-geojson
EmbeddedFinder exports the same GeoJSON, decoding only the requested
timezone’s rings from the file on demand.
For now, tzf-rs’ binding in Wasm, named
tzf-wasm, has exported this feature
and it has been deployed to the tzf-web
for online usage.
Migrating from v1
v1 loaded protobuf artifacts (CompressedTopoTimezones, PreindexTimezones);
those artifacts are no longer published, and v2 removes every protobuf-typed
API. Mappings:
| v1 |
v2 |
DefaultFinder::new() |
unchanged |
DefaultFinder::new_full() |
unchanged (full feature, git-only data) |
DefaultFinder::{get_tz_name,get_tz_names,timezonenames,data_version} |
unchanged |
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) / from_pb(pb) |
DefaultFinder::from_tzb(&[u8]) |
FuzzyFinder::from_pb(pb) |
removed — no separate tile-only finder |
FinderOptions / *_with_options |
removed — YStripes is always on |
tzf_rs::pbgen module |
removed — no protobuf types in the public API |
tzf_rs::revert_timezones(&pb) |
removed — took a protobuf type |
finder.finder.get_tz_geojson(...) |
finder.get_tz_geojson(...) |
FuzzyFinder::to_geojson() |
DefaultFinder::to_preindex_geojson() -> Option<BoundaryFile> |
FuzzyFinder::get_tz_geojson(name) -> Option<FeatureItem> |
DefaultFinder::get_tz_preindex_geojson(name) -> Option<BoundaryFile> |
feature bundled (pb lite data) |
feature bundled (lite .tzb) |
feature full (pb full data, git-only) |
feature full (full .tzb, still git-only) |
features clap, export-geojson |
unchanged |
| — |
new: EmbeddedFinder, tzf_rs::Error |
Behavior changes:
get_tz_names results are now sorted lexicographically.
get_tz_name on DefaultFinder answers from the preindex tile when one
covers the point (v1 DefaultFinder semantics; v1 Finder users who need
polygon-exact multi-results use get_tz_names).
- The byte constructors return
Result<_, tzf_rs::Error>: files are
CRC-checked and structurally validated at open, instead of silently
yielding an empty finder.
- New:
EmbeddedFinder, an in-place low-memory mechanism (~4 MB total).
- MSRV is now 1.88.
Accuracy
The Douglas-Peucker simplification uses an epsilon of 0.001 degrees, which
caps boundary displacement at roughly 111 m by construction. Measured against
the full-precision 2026c dataset with tzf’s internal/cmd/borderchange
(spherical model, certified via Lipschitz interval subdivision):
| Metric |
Result |
| Certified maximum boundary displacement |
111.7 m (+1.0 m tolerance) |
| Boundary length displaced more than 100 m |
0.41% |
| Boundary length displaced more than 500 m |
0% |
| Total mis-assigned area |
16,962 km² (~0.003% of Earth) |
| Mis-assigned area within 100 m of the true border |
92.8% |
See BORDER_CHANGE.md
in the tzf repository for the complete evaluation results.
Only queries that land within ~111 m of a timezone border can differ from the
full-precision result, and most of that band is far narrower. If your use case
is sensitive inside that band, enable the full feature and use
DefaultFinder::new_full().
The tzf-rs package is intended for high-performance geospatial query services,
such as weather forecasting APIs. Most queries can be returned within a very
short time, averaging around 150-300 nanoseconds with DefaultFinder.
Here is what has been done to improve performance:
- Using the simplified dataset by default.
- Using pre-indexing (the
.tzb FUZZY section) to handle most queries in
about 100 nanoseconds.
- Using a finely-tuned Ray Casting algorithm package
ringsaturn/geometry-rs to
verify whether a polygon contains a point.
- Using YStripes(inspired by Josh Baker’s
tg’s ) to accerate polygon queries. This
polygon index works when the pre-indexing missing, especially for queries
around the border.
- Also the dense 1°×1° grid index carried by the
.tzb file to quickly
find candidate polygons, inspired by Aaron Roney’s
rtz.
That’s all. There are no black magic tricks inside the tzf-rs.
Benchmark numbers (Apple M3 Max, bundled lite dataset 2026c, make bench):
| Target |
Scenario |
Median estimate |
| DefaultFinder |
get_tz_name, 154,248-city sweep |
25.1 ms total (~163 ns/query) |
| EmbeddedFinder |
get_tz_name, 154,248-city sweep |
147.0 ms total (~950 ns/query) |
| DefaultFinder |
get_tz_name, edge city (preindex miss) |
434 ns |
| EmbeddedFinder |
get_tz_name, edge city (preindex miss) |
4.03 µs |
| DefaultFinder |
get_tz_names, edge city |
531 ns |
| EmbeddedFinder |
get_tz_names, edge city |
5.71 µs |
| DefaultFinder |
open (new()) |
13.0 ms |
| EmbeddedFinder |
open (new()) |
2.05 ms |
The whole-dataset sweep is the number to compare against: the per-query
*_random_city benches draw a single random city and reuse it for the whole
measurement, so their absolute value varies with the draw.
tzf-rs consumes the .tzb profile only. The .tzm memory image the Go
runtime uses exists for zero-copy ring aliasing, which geometry-rs’s owned
polygon storage cannot exploit — measured here it saved ~3 ms of open time
while costing more memory, so it is not supported.
Peak RSS (macOS, make memory): ~44 MiB for DefaultFinder (v1 needed
~82 MiB), ~6 MiB for EmbeddedFinder.
You can view more details from latest benchmark from
GitHub Actions logs.
References
I have written an article about the history of tzf, its Rust port, and its
Rust port’s Python binding; you can view it
here.
See Project tzf for
more information.
Bindings
Command line
The binary helps in debugging tzf-rs and using it in (scripting) languages
without bindings. Either specify the coordinates as parameters to get a single
time zone, or to look up multiple coordinates efficiently specify the ordering
and pipe them to the binary one pair of coordinates per line.
tzf --lng 116.3883 --lat 39.9289
echo -e "116.3883 39.9289\n116.3883, 39.9289" | tzf --stdin-order lng-lat
If you are using Nixpkgs, you can install the tzf command line tool, please
see more in
Nixpkgs.
LICENSE
This project is licensed under the MIT license. The data is
licensed under the
ODbL license,
same as
evansiroky/timezone-boundary-builder

评论