It has been a few years since the tzf project family was started. The last systematic look back at its development history was History of package tzf in early 2023. Since then, there have been various updates and maintenance work, mostly focused on non-core optimizations and supplementary features.
In spring 2026, several long-pending important changes were finally completed:
- Introducing topology-aware processing to eliminate gaps and overlaps introduced during polygon simplification;
- Based on topology-aware processing, developing a more efficient data distribution format — ~17 MB for full-precision data and ~5.4 MB for simplified data;
- Introducing YStripes index acceleration, inspired by the tg project.
Topology-Aware Processing
The raw data is essentially a collection of polygons. Because the raw boundaries are highly detailed, the data volume is large, so polygon simplification is necessary. Many of these polygons share boundaries, but in the previous approach, each polygon was simplified independently using RDP. This caused a known issue that existed since the project’s early days: gaps appearing in areas that should be fully covered, and unwanted polygon overlaps introduced by simplification:
See details in 
ringsaturn/tzf#183
The solution had been clear for years: first identify shared boundaries, simplify those shared boundaries, then substitute the simplified boundaries back into the polygons on both sides. This ensures adjacent polygons continue to reference the same simplified boundary, preventing gaps or overlaps caused by independent simplification on each side.
The problem was that the dataset is very large. Over the past few years I made multiple attempts to implement this strategy by hand, and all of them failed. Accumulating edge cases and increasingly complex strategy design made the code impossible to run stably.
When I tried again in 2026, I used Claude and Codex across multiple rounds of implementation, verification, and refactoring, and finally got the complete strategy working. The rough flow is illustrated below:
Made by ChatGPT
With this strategy in place, it also became possible to implement the new data storage format goals designed last year.
To maintain backward compatibility, the new binary data has been split into a new repository to carry the format improvements described below. The existing data format distribution — the tzf-rel series — will continue for a while before being deprecated.
Since shared boundaries can now be identified, there is no need to store lengthy boundaries twice; they are stored once and encoded with polyline compression.
The effect is significant. Previously, tzf distributed the full dataset in pb format at roughly 90 MB uncompressed and ~50 MB zipped. Now, with shared boundaries stored only once and polyline-encoded, the full-precision data is ~17 MB, or ~10 MB zipped. I’m quite satisfied that full-precision data can be compressed to this size. It is also precisely because of this acceptable file size that tzf-rs now finally offers an optional feature to support the full dataset. Previously, due to the 90 MB size, users had to download the full dataset themselves and provide the file path.
For the simplified dataset, omitting polyline compression would actually cause a slight size increase. The reason is that many small polygon details that were previously discarded are now retained under new criteria for precision reasons. On the other hand, because the boundaries themselves have already been greatly simplified, the benefit of storing shared boundaries only once is less pronounced than with full-precision data. Currently, with shared-boundary detection and polyline processing, the simplified dataset is ~5.4 MB, which is still acceptable.
One thing worth noting: when tzf uses full-precision data, runtime memory usage is around 500 MB, which is significant — there are no plans to optimize this further for now, and this feature will not be brought down to the Python binding for the time being. Even with the simplified dataset, around 100 MB of memory is needed. The tzf family — especially the Go, Rust, and Python versions — was designed from the start for high-concurrency backend API scenarios, where a certain memory footprint is acceptable in exchange for near-zero-latency lookups and boundary accuracy that cannot be overly simplified. Memory usage, processing speed, and data precision all need to be balanced together. What to use and how to use it ultimately depends on each user’s actual requirements.
For more details on this feature, refer to the code documentation at internal/topology/README.md.
Current data files are as follows:
| File | Size | Description |
|---|---|---|
combined-with-oceans.compress.topo.bin | ~17MB | Full precision: shared-edge dedup + polyline compression |
combined-with-oceans.topology.compress.topo.bin | ~5.4MB | Lite: topology-aware simplify + shared-edge dedup + polyline compression |
combined-with-oceans.reduce.preindex.bin | ~2MB | Tile pre-index for FuzzyFinder |
YStripes Index
To be clear: the YStripes index is not my invention. It comes from Josh Baker’s tidwall/tg project. I simply ported this indexing mechanism into the Go and Rust versions of tzf.
Starting this spring, this index has become the default strategy for the Go and Rust versions of tzf. It does add some memory overhead, but the performance gains are more substantial. In my local benchmarks, a single random lookup has come down to around 1 microsecond, which should not be a bottleneck in any of the use cases I am aware of.
I won’t go into the algorithm details here — if you’re interested, you can read the author’s explanation directly in POLYGON_INDEXING.md.
GridIndex
YStripes speeds up segment traversal inside a single PIP check; but DefaultFinder.GetTimezoneName had another performance hole: FuzzyFinder handles about 95% of queries, while the remaining ~5% of boundary cases fall back to Finder for a full linear scan across ~444 timezones.
The fix is a 1°×1° grid index (GridIndex): divide the world into 360×180 = 64,800 cells, precompute at build time which timezone bounding boxes intersect each cell, and embed the index directly into the .compress.topo.bin file. At query time, an O(1) map lookup reduces the candidate set from ~444 to typically 1–3 timezones, which are then checked with precise PIP. For cells with a single candidate far from extreme latitudes and longitudes, the PIP step is skipped entirely via a short-circuit.
The idea is inspired by twitchax/rtz’s precomputed grid lookup, adapted into tzf’s protobuf data pipeline. The index travels with the data file transparently — old files without a GridIndex fall back to the full linear scan with no API changes required.
Measured results (Apple M3 Max, go test -bench):
| Scenario | Before | After | Speedup |
|---|---|---|---|
| Edge case (lite) | 2108 ns/op | 1060 ns/op | 2.0× |
| Random cities (lite) | 1742 ns/op | 452 ns/op | 3.9× |
| Edge case (full, no preindex) | 2057 ns/op | 1019 ns/op | 2.0× |
| Random cities (full, no preindex) | 1955 ns/op | 606 ns/op | 3.2× |
With GridIndex in place, the performance gap that FuzzyFinder was filling on the fallback path has narrowed significantly, though the two-tier structure is kept for now pending further evaluation of whether to simplify to a single Finder + GridIndex. Perhaps a more significant architectural adjustment will come in the future V2 series, where the entire lookup strategy may be redesigned to better leverage the new data format and indexing capabilities.
This optimization ships in tzf Go v1.2.0 and tzf-rs v1.3.3, with the Rust side implementing equivalent grid index support against the same shared proto schema.
Benchmark
Here are my local benchmark results, run on a MacBook Pro with Apple M3 Max.
These results are primarily for observing relative differences between strategies and should not be taken as absolute cross-machine performance conclusions.
tzf (Go)
| Target | Dataset | Scenario | Median (ns) | p99 (ns) | Approx throughput (ops/s) | Memory (MiB) |
|---|---|---|---|---|---|---|
| DefaultFinder | topology-simplified + preindex | edge case · GetTimezoneName | 3000.0 | 3000.0 | 393.5K | 74.70 |
| Finder | topology-simplified | edge case · GetTimezoneName | 2000.0 | 3000.0 | 470.4K | 66.00 |
| FullFinder | full-precision + preindex | edge case · GetTimezoneName | 3000.0 | 3000.0 | 395.6K | 421.50 |
| Finder | full-precision | edge case · GetTimezoneName | 2000.0 | 3000.0 | 475.3K | 412.70 |
| DefaultFinder | topology-simplified + preindex | random world cities · GetTimezoneName | 1000.0 | 4000.0 | 1162.4K | 74.70 |
| FuzzyFinder | preindex | random world cities · GetTimezoneName | 469.8 | 1000.0 | 2128.6K | 8.90 |
| Finder | topology-simplified | random world cities · GetTimezoneName | 2000.0 | 4000.0 | 531.6K | 66.00 |
| FullFinder | full-precision + preindex | random world cities · GetTimezoneName | 1000.0 | 4000.0 | 1143.1K | 421.50 |
| Finder | full-precision | random world cities · GetTimezoneName | 2000.0 | 5000.0 | 468.6K | 412.70 |
| DefaultFinder | topology-simplified + preindex | random world cities · GetTimezoneNames | 5000.0 | 9000.0 | 208.0K | 74.70 |
| FuzzyFinder | preindex | random world cities · GetTimezoneNames | 462.7 | 1000.0 | 2161.2K | 8.90 |
| Finder | topology-simplified | random world cities · GetTimezoneNames | 5000.0 | 8000.0 | 211.5K | 66.00 |
| FullFinder | full-precision + preindex | random world cities · GetTimezoneNames | 5000.0 | 9000.0 | 192.8K | 421.50 |
tzf-rs (Rust)
Topology-Simplified (bundled):
| Target | Dataset | Scenario | Median estimate (µs) | Approx throughput (ops/s) | Avg peak RSS (MiB) |
|---|---|---|---|---|---|
| Finder | topology-simplified | YStripes only | 1.2296 | 813,273 | 103.30 |
| Finder | topology-simplified | No index | 6.5402 | 152,901 | 51.68 |
| DefaultFinder | topology-simplified + preindex | YStripes only | 1.1383 | 878,503 | 125.98 |
| DefaultFinder | topology-simplified + preindex | No index | 2.2514 | 444,168 | 77.79 |
Full-Precision (full):
| Target | Dataset | Scenario | Median estimate (µs) | Approx throughput (ops/s) | Avg peak RSS (MiB) |
|---|---|---|---|---|---|
| Finder (full) | full-precision | YStripes only | 2.0852 | 479,570 | 561.08 |
| Finder (full) | full-precision | No index | 37.6980 | 26,527 | 252.54 |
| DefaultFinder (full) | full-precision + preindex | YStripes only | 1.3488 | 741,400 | 584.30 |
| DefaultFinder (full) | full-precision + preindex | No index | 11.2750 | 88,692 | 278.63 |
Python
The Python version is primarily a binding, so benchmark results are omitted here. Worth mentioning though: the wheel size dropped from ~7 MB to ~4 MB, which is a small but welcome improvement for image build artifacts.
Continuous Benchmark in GitHub Actions
Below are long-term performance metrics monitored through Continuous Benchmark:
tzf ns/op
tzf-rs ns/iter
tzf iter/sec
End
That covers the main features completed during this busy spring. For the tzf project family, this update fills in several key pieces of the original design: using Go to perform topology-aware polygon dataset simplification and distribution, letting the Go, Rust, Python, and other language versions reuse the same data output, and now closing the fallback-path performance gap with the grid index. tzf-swift also picked up the same round of updates (v1.2.2), keeping all language ports in sync.
The tzfpy grid index update will ship alongside the next dataset release and is not yet on PyPI.
Ongoing maintenance will be relatively light, focusing mainly on data file updates, dependency updates, and minor interface compatibility work.
The development above was spread across different time periods. Corresponding releases for reference:
- https://github.com/ringsaturn/geometry-rs/releases/tag/v0.4.1
- https://github.com/ringsaturn/tzf-rs/releases/tag/v1.2.0
- https://github.com/ringsaturn/tzf-rs/releases/tag/v1.3.0
- https://github.com/ringsaturn/tzf-rs/releases/tag/v1.3.3
- https://github.com/ringsaturn/tzfpy/releases/tag/v1.2.0
- https://github.com/ringsaturn/tzfpy/releases/tag/v1.3.0
- https://github.com/ringsaturn/tzf/releases/tag/v1.1.0
- https://github.com/ringsaturn/tzf/releases/tag/v1.2.0
- https://github.com/ringsaturn/tzf-swift/releases/tag/v1.2.2
- https://github.com/ringsaturn/tzf-dist/releases/tag/v0.0.2026-a
