tzf: a fast timezone finder for Go. [](https://pkg.go.dev/github.com/ringsaturn/tzf) [](https://codecov.io/gh/ringsaturn/tzf) [](https://app.fossa.com/projects/git%2Bgithub.com%2Fringsaturn%2Ftzf?ref=badge_shield)

July 29, 2026 · View on GitHub

Quick Start

Install via:

go get github.com/ringsaturn/tzf

Note

This NewDefaultFinder uses simplified shape data so it is not entirely accurate around the border, but the error is small and bounded: every simplified boundary stays within ~111 m of the full-precision border. See Accuracy for measured numbers.

It's expensive to init tzf's Finder/FuzzyFinder/DefaultFinder, please consider reuse it or as a global var. Below is a global var example:

package main

import (
	"fmt"

	"github.com/ringsaturn/tzf"
)

var f tzf.F

func init() {
	var err error
	f, err = tzf.NewDefaultFinder()
	if err != nil {
		panic(err)
	}
}

func main() {
	// In longitude-latitude order
	fmt.Println(f.GetTimezoneName(116.3883, 39.9289))
	fmt.Println(f.GetTimezoneName(-73.935242, 40.730610))
}

Best Practice: Setup 100% Accuracy via NewFullFinder

If you require a query result that is 100% accurate, use the following to locate(also, reuse it when possible):

package main

import (
	"fmt"

	"github.com/ringsaturn/tzf"
)

func main() {
	finder, err := tzf.NewFullFinder()
	if err != nil {
		panic(err)
	}

	fmt.Println(finder.GetTimezoneName(139.6917, 35.6895))
}

Please note that NewFullFinder() is more expensive to init and has higher memory usage than NewDefaultFinder(), but it provides 100% accuracy.

See the Performance section for more details.

CLI Tool

In addition to using tzf as a library in your Go projects, you can also use the tzf command-line interface (CLI) tool to quickly get the timezone name for a set of coordinates. To use the CLI tool, you first need to install it using the following command:

go install github.com/ringsaturn/tzf/cmd/tzf@latest

Once installed, you can use the tzf command followed by the latitude and longitude values to get the timezone name:

tzf -lng 116.3883 -lat 39.9289

Alternatively if you want to look up multiple coordinates efficiently you can specify the ordering and pipe them to the tzf command one pair of coordinates per line:

echo -e "116.3883 39.9289\n116.3883, 39.9289" | tzf -stdin-order lng-lat

Data

You can download the original data from https://github.com/evansiroky/timezone-boundary-builder.

The preprocessed protobuf data can be obtained from https://github.com/ringsaturn/tzf-dist, which has Go's embedded support. These files are Protocol Buffers messages for more efficient binary distribution. You can view the pb/tzinfo.proto file or its HTML format documentation for information about the internal format.

The data pipeline for tzf can be illustrated as follows:

graph TD
    Raw[GeoJSON from evansiroky/timezone-boundary-builder]
    Full[Timezones .bin ~92MB]
    Simplified[Timezones .topology.bin ~13MB<br/>topology-aware simplified]
    SimplifiedTopo[TopoTimezones .topology.topo.bin ~10MB]
    FullTopo[TopoTimezones .topo.bin ~52MB]
    SimplifiedCompressTopo[CompressedTopoTimezones<br/>.topology.compress.topo.bin ~5.4MB]
    FullCompressTopo[CompressedTopoTimezones<br/>.compress.topo.bin ~17MB]
    Preindex[PreindexTimezones<br/>.topology.preindex.bin ~2MB]

    Finder[Finder: Polygon Based Finder]
    FuzzyFinder[FuzzyFinder: Tile based Finder]
    DefaultFinder[DefaultFinder: FuzzyFinder + Finder fallback]

    Raw --> |cmd/geojson2tzpb|Full
    Full --> |cmd/reducetzpb -topology|Simplified
    Full --> |cmd/deduplicatetzpb|FullTopo
    FullTopo --> |cmd/compresstopotzpb|FullCompressTopo
    Simplified --> |cmd/deduplicatetzpb|SimplifiedTopo
    SimplifiedTopo --> |cmd/compresstopotzpb|SimplifiedCompressTopo
    Simplified --> |cmd/preindextzpb|Preindex

    FullCompressTopo --> |tzf.NewFinderFromCompressedTopo|Finder
    SimplifiedCompressTopo --> |tzf.NewFinderFromCompressedTopo|Finder
    Preindex --> |tzf.NewFuzzyFinderFromPB|FuzzyFinder
    SimplifiedCompressTopo --> |tzf.NewDefaultFinder|DefaultFinder
    Preindex --> |tzf.NewDefaultFinder|DefaultFinder
    FullCompressTopo --> |tzf.NewFullFinder|DefaultFinder
    Preindex --> |tzf.NewFullFinder|DefaultFinder

The combined-with-oceans.compress.topo.bin (~17MB) preserves full geometric precision with shared-edge deduplication and polyline compression. Use NewFullFinder() to load it.

The combined-with-oceans.topology.compress.topo.bin (~5.4MB) applies topology-aware Douglas-Peucker simplification (~85% point reduction) before deduplication and compression. It is used by the default NewDefaultFinder() and may not be perfectly accurate at some border areas; the deviation is bounded to ~111 m (see Accuracy).

The combined-with-oceans.topology.preindex.bin (~2MB) consists of multiple map tiles and is used within both DefaultFinder and FullFinder as the fast-path FuzzyFinder, handling most queries without polygon ray-casting.

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.

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 internal/cmd/borderchange (spherical model, certified via Lipschitz interval subdivision):

MetricResult
Certified maximum boundary displacement111.2 m (+1.0 m tolerance)
Boundary length displaced more than 100 m0.41%
Boundary length displaced more than 500 m0%
Total mis-assigned area16,828 km² (~0.003% of Earth)
Mis-assigned area within 100 m of the true border92.8%

In other words, only queries that land within ~111 m of a timezone border can ever differ from the full-precision result, and most of that band is far narrower. If your use case is sensitive inside that band, use NewFullFinder().

Verify the accuracy yourself by running the following commands:

gh release download v0.0.2026-c-fix1 --repo ringsaturn/tzf-dist \
  --pattern "combined-with-oceans.compress.topo.bin"

go run ./internal/cmd/topodecode \
  combined-with-oceans.compress.topo.bin combined-with-oceans.dist.bin

go run ./internal/cmd/borderchange \
  -epsilon 0.001 \
  -certification-tolerance-m 0.5 \
  -top-pairs 20 \
  combined-with-oceans.dist.bin > BORDER_CHANGE.md

Output like:

➜  tzf git:(main) ✗ go run ./internal/cmd/borderchange \
  -epsilon 0.001 \
  -certification-tolerance-m 0.5 \
  -top-pairs 20 \
  combined-with-oceans.dist.bin > BORDER_CHANGE.md
2026/07/13 10:46:00 borderchange: dataset loaded in 441ms
2026/07/13 10:46:15 borderchange: simplification finished in 14.276s
2026/07/13 10:46:16 borderchange: 532881 arcs collected, analyzing with 16 workers

More details: BORDER_CHANGE.md.

Performance

The tzf package is intended for high-performance geospatial query backend services, such as weather forecasting APIs. Most queries can be returned within a very short time, averaging around 1000 nanoseconds.

Here is what has been done to improve performance:

  1. Using the simplified dataset by default.
  2. Using pre-indexing to handle most queries takes approximately 500 nanoseconds.
  3. Using the internal geom package(fork of geojson) with a YStripes index (inspired by Josh Baker's tg's ) to verify whether a polygon contains a point. Also a grid-index to quickly find candidate polygons, inspired by Aaron Roney's rtz.

That's all. There are no black magic tricks inside the tzf package.

Below is a benchmark run on my MacBook Pro with Apple M3 Max:

TargetDatasetScenarioMedian (ns)p99 (ns)Approx throughput (ops/s)Memory (MiB)
DefaultFindertopology-simplified + preindexedge case · GetTimezoneName583.01708.01439.3K31.90
FuzzyFinderpreindexedge case · GetTimezoneName250.0500.02682.4K2.40
Findertopology-simplifiededge case · GetTimezoneName416.02291.01734.0K29.70
FullFinderfull-precision + preindexedge case · GetTimezoneName625.02083.01317.7K155.30
Finderfull-precisionedge case · GetTimezoneName416.02000.01827.5K153.00
DefaultFindertopology-simplified + preindexrandom world cities · GetTimezoneName208.0959.03499.0K31.90
FuzzyFinderpreindexrandom world cities · GetTimezoneName208.0416.03461.4K2.40
Findertopology-simplifiedrandom world cities · GetTimezoneName292.01708.02275.8K29.70
FullFinderfull-precision + preindexrandom world cities · GetTimezoneName208.01542.03003.0K155.30
Finderfull-precisionrandom world cities · GetTimezoneName291.01500.02498.8K153.00
Findertopology-simplified + GridIndexrandom world cities · GetTimezoneName250.01459.02606.9K29.70
Findertopology-simplified (no GridIndex)random world cities · GetTimezoneName2292.04208.0473.0K24.00
DefaultFindertopology-simplified + preindexrandom world cities · GetTimezoneNames625.03250.0888.1K31.90
FuzzyFinderpreindexrandom world cities · GetTimezoneNames209.0541.02751.8K2.40
Findertopology-simplifiedrandom world cities · GetTimezoneNames542.02416.01377.6K29.70
FullFinderfull-precision + preindexrandom world cities · GetTimezoneNames583.02458.01312.9K155.30
Language or SeverLinkNote
Goringsaturn/tzf
RubyHarlemSquirrel/tzf-rbbuild with tzf-rs
Rustringsaturn/tzf-rs
Swiftringsaturn/tzf-swift
Pythonringsaturn/tzfpybuild with tzf-rs
HTTP APIracemap/rust-tz-servicebuild with tzf-rs
JS via Wasm(browser only)ringsaturn/tzf-wasmbuild with tzf-rs
Onlineringsaturn/tzf-webbuild with tzf-wasm

See Project tzf for more information.

Thanks

LICENSE

This project is licensed under the MIT license and Anti CSDN License1. The data is licensed under the ODbL license, same as evansiroky/timezone-boundary-builder

FOSSA Status

Footnotes

  1. This license is to prevent the use of this project by CSDN, has no effect on other use cases.