Skip to content

Latest commit

 

History

430 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tzf: a fast timezone finder for Go. Go Reference codecov FOSSA Status

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
Loading

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):

Metric Result
Certified maximum boundary displacement 111.2 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,828 km² (~0.003% of Earth)
Mis-assigned area within 100 m of the true border 92.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:

Target Dataset Scenario Median (ns) p99 (ns) Approx throughput (ops/s) Memory (MiB)
DefaultFinder topology-simplified + preindex edge case · GetTimezoneName 583.0 1708.0 1439.3K 31.90
FuzzyFinder preindex edge case · GetTimezoneName 250.0 500.0 2682.4K 2.40
Finder topology-simplified edge case · GetTimezoneName 416.0 2291.0 1734.0K 29.70
FullFinder full-precision + preindex edge case · GetTimezoneName 625.0 2083.0 1317.7K 155.30
Finder full-precision edge case · GetTimezoneName 416.0 2000.0 1827.5K 153.00
DefaultFinder topology-simplified + preindex random world cities · GetTimezoneName 208.0 959.0 3499.0K 31.90
FuzzyFinder preindex random world cities · GetTimezoneName 208.0 416.0 3461.4K 2.40
Finder topology-simplified random world cities · GetTimezoneName 292.0 1708.0 2275.8K 29.70
FullFinder full-precision + preindex random world cities · GetTimezoneName 208.0 1542.0 3003.0K 155.30
Finder full-precision random world cities · GetTimezoneName 291.0 1500.0 2498.8K 153.00
Finder topology-simplified + GridIndex random world cities · GetTimezoneName 250.0 1459.0 2606.9K 29.70
Finder topology-simplified (no GridIndex) random world cities · GetTimezoneName 2292.0 4208.0 473.0K 24.00
DefaultFinder topology-simplified + preindex random world cities · GetTimezoneNames 625.0 3250.0 888.1K 31.90
FuzzyFinder preindex random world cities · GetTimezoneNames 209.0 541.0 2751.8K 2.40
Finder topology-simplified random world cities · GetTimezoneNames 542.0 2416.0 1377.6K 29.70
FullFinder full-precision + preindex random world cities · GetTimezoneNames 583.0 2458.0 1312.9K 155.30

Related Repos

Language or Sever Link Note
Go ringsaturn/tzf
Ruby HarlemSquirrel/tzf-rb build with tzf-rs
Rust ringsaturn/tzf-rs
Swift ringsaturn/tzf-swift
Python ringsaturn/tzfpy build with tzf-rs
HTTP API racemap/rust-tz-service build with tzf-rs
JS via Wasm(browser only) ringsaturn/tzf-wasm build with tzf-rs
Online ringsaturn/tzf-web build 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.

About

Get timezone via longitude and latitude in Go in a fast way

Topics

Resources

Code of conduct

Stars

180 stars

Watchers

2 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages