Repository navigation
v2.0.0
v2.0.0
tzf v2 is protobuf-free. The runtime no longer decodes protobuf on any code
path: boundary data ships as the TZF embedded binary format (.tzb) and its
memory-image profile (.tzm), and the public surface is five constructors
that all return the tzf.F interface.
Measured on Apple M3 Max with the 2026c dataset: the default finder's
retained heap is 12.8 MiB, against ~32 MB for the v1 default (geometry is
stored in read-only data and aliased in place instead of being copied to the
heap). Load time is about 5× lower than the protobuf path (lite 18.6 ms
against 96 ms; full 78.5 ms against 288 ms). The new NewEmbeddedFinder
queries the 3.97 MB file in place with under 1 KB of heap.
Module path is now github.com/ringsaturn/tzf/v2.
go get github.com/ringsaturn/tzf/v2Public surface
Five constructors, all returning tzf.F. The mechanism behind a finder is not
part of the API, and every artifact carries the same dataset (2026c, 444
timezone names). Latency and memory figures are from make bench and
make bench-memory on Apple M3 Max, 2026c:
| Constructor | Mechanism | Memory | Query (p50, random / border) |
|---|---|---|---|
NewDefaultFinder() |
lite .tzm memory image, polygon storage aliasing rodata |
12.8 MiB heap + ~10 MB rodata | 208 ns / 542 ns |
NewEmbeddedFinder() |
lite .tzb queried in place, no geometry expansion |
<1 KB heap + ~4 MB rodata | 583 ns / 6.9 µs |
NewFullFinder() |
full-precision .tzb expanded at load |
146.7 MiB heap | 208 ns / 666 ns |
NewFinderFromTZB(data) |
any .tzb, always expanded; data released after load |
27.5 MiB heap (lite dataset) | 208 ns |
NewFinderFromTZM(data) |
any .tzm, always aliased in place; data retained |
as NewDefaultFinder |
208 ns |
Open times on the same machine and dataset, 16 cores / 1 core (the
single-core figure models a cgroup-quota pod): lite .tzb in place
1.7 / 1.7 ms; lite .tzm 7.7 / 28 ms; lite .tzb expanded 18.6 / ~41 ms;
full .tzb expanded 78.5 / 214 ms.
Also public:
tzf.GeoJSONer:GetTZGeoJSON,GetGeoJSON,GetTZPreindexGeoJSON,
GetPreindexGeoJSON.Fcovers the four query methods only, so this is a
separate interface and test doubles and third-partyFimplementations need
not produce geometry; assert it on the value a constructor returned
(f.(tzf.GeoJSONer)). Every finder this package builds satisfies it,
including the in-place one, which decodes only the requested timezone's
rings on demand. Output is byte-identical across mechanisms.tzf.ErrNoTimezoneFound/tzf.ErrNoFuzzySection: sentinel errors.github.com/ringsaturn/tzf/v2/x: experimental surface holding
NewFinderFromTZBReaderAt(source io.ReaderAt, size int64)for in-place
queries over a file, an mmap'd region, or abytes.Readerover
caller-owned bytes. It is exempt from the module's semver promise; see
Known caveats.
GetTimezoneName is fuzzy-first: a preindex tile resolves most queries with
no point-in-polygon work. GetTimezoneNames runs the polygon scan in every
finder and is the call to use when a point can belong to more than one
timezone.
Removed API
Every protobuf-typed constructor is gone, together with the per-mechanism
constructors and the tile-only finder:
NewFinderFromPB,NewFinderFromCompressed,NewFinderFromCompressedTopo,
NewFuzzyFinderFromPB,NewFinderFromRawJSONNewFinderFromTZBExpanded,NewFuzzyFinderFromTZB,
NewDefaultFinderFromTZB,NewDefaultFinderFromTZM, and the root-package
NewFinderFromTZBReaderAt(moved tox)- the exported finder types
Finder,FuzzyFinder,DefaultFinder;
constructors return theFinterface FuzzyFinderin any form: there is no tile-only finder, no
NewFuzzyFinder*, and no replacement for a finder that resolves queries
from tiles alone. The preindex remains as the internal fast path inside
every finder, and its tiles are exportable as GeoJSON throughGeoJSONer.- the public pipeline packages
reduce,preindex, andconvert.Do; the
pipeline is internal and is driven through thecmd/binaries GetTZGeoJSON/GetGeoJSONreturn serialized GeoJSON bytes; the
*convert.BoundaryFilereturn type is gone and the boundary-file types are
internal
Data source change
tzf-dist now ships three artifacts, all carrying one data_version and each
bundling its own FUZZY preindex section, so there is no separate preindex
artifact to keep in sync:
| Artifact | Size | Backs |
|---|---|---|
lite.tzb |
3.97 MB | NewEmbeddedFinder, x, generic TZB use |
lite.tzm |
10.18 MB | NewDefaultFinder |
full.tzb |
13.77 MB | NewFullFinder |
- The protobuf artifacts stop being published. Once this set ships,
tzf-distno longer builds or publishes v1's protobuf artifacts, and the
v1 line is frozen at its last data release: staying on v1 means staying on
that data version, and updated boundaries require moving to v2. .tzbis the transport format. No full-precision.tzmis distributed;
derive one locally withcmd/tzb2tzm(tzb2tzm -o out.tzm in.tzb), whose
output is byte-identical to encoding the.tzmfrom source. The compact
.tzbcan be shipped and the memory image produced where it is consumed.- The data build runs directly from the upstream raw GeoJSON, with no
protobuf step at any stage; intermediates are gob and are never distributed.
Rebuilding from GeoJSON was verified equivalent to the protobuf-era
artifacts (full.tzbbyte-identical).
Boundary accuracy is unchanged: the lite dataset's simplification is bounded
at ~111 m (certified maximum 111.7 m; 0.41% of boundary length displaced more
than 100 m; 0% more than 500 m). See BORDER_CHANGE.md.
Migration
Update the import path (github.com/ringsaturn/tzf →
github.com/ringsaturn/tzf/v2), then map the call sites:
| v1 | v2 |
|---|---|
tzf.NewDefaultFinder() |
tzf.NewDefaultFinder() (unchanged call sites; now backed by lite.tzm) |
tzf.NewFullFinder() |
tzf.NewFullFinder() (unchanged call sites) |
*tzf.Finder, *tzf.DefaultFinder (named types) |
removed; constructors return tzf.F |
tzf.FuzzyFinder, tzf.NewFuzzyFinderFromPB |
removed, no replacement; the preindex is the fast path inside every finder |
tzf.NewFinderFromCompressedTopo(pb) |
tzf.NewFinderFromTZB(data) |
tzf.NewFinderFromCompressed(pb) |
tzf.NewFinderFromTZB(data) |
tzf.NewFinderFromPB(pb) |
convert offline with cmd/geojson2tzpb … cmd/topo2embed, then NewFinderFromTZB |
tzf.NewFinderFromRawJSON(...) |
removed; build a .tzb with the pipeline commands instead |
tzf.NewFinderFromTZBExpanded(data) |
tzf.NewFinderFromTZB(data) (expansion is the only TZB behavior) |
tzf.NewDefaultFinderFromTZB/TZM, NewFuzzyFinderFromTZB |
tzf.NewFinderFromTZB / tzf.NewFinderFromTZM; composition is derived from the file |
tzf.NewFinderFromTZBReaderAt(r, size) |
x.NewFinderFromTZBReaderAt(r, size); see the x stability policy |
| in-place querying over caller-owned bytes | x.NewFinderFromTZBReaderAt(bytes.NewReader(data), int64(len(data))); same policy |
f.(*tzf.Finder).GetTZGeoJSON(name) |
f.(tzf.GeoJSONer).GetTZGeoJSON(name) (returns bytes) |
FuzzyFinder tile-bbox GeoJSON |
f.(tzf.GeoJSONer).GetTZPreindexGeoJSON(name) / GetPreindexGeoJSON() |
convert.Do, convert.Revert, reduce, preindex |
internal; the pipeline is driven through the cmd/ binaries |
Behavior changes:
GetTimezoneNamesresults are sorted lexicographically.- A point exactly on a shared border belongs to every touching polygon
(exterior rings allow on-edge, hole rings do not). - v1's tile-bbox GeoJSON on
FuzzyFinderis replaced by the preindex export
onGeoJSONer, which covers the same tiles from any finder that carries a
FUZZY section.
Known caveats
xpackage stability: withinv2.y.z, a bump ofymay change or remove
anything inx; onlyzbumps are guaranteed not to. The root package
keeps the normal promise, with breaking changes only atv3. The convention
followsgolang.org/x/…; pin an exact version if you depend onxand
cannot absorb a break at a minor release..tzmalignment:NewFinderFromTZMaliases the file's point section as
polygon storage on little-endian hosts, which requires the slice to be
8-byte aligned. A misaligned slice or a big-endian host falls back to a
one-time decoded copy, which produces the same results and does not reduce
memory.tzf-distpins the alignment of its embeddedlite.tzmwith a
regression test, so a toolchain change fails a test instead of doubling
NewDefaultFinder's footprint.NewFinderFromTZMretains its input: the byte slice is used directly as
polygon storage and must stay live and unmodified for the finder's lifetime.
Passgo:embedbytes or an mmap'd region.NewFinderFromTZBreleases
dataafter loading.NewEmbeddedFinderlatency: preindex hits stay under 1 µs, and a miss scans
the compressed geometry at about 6.9 µs on border cases against 542 ns for
NewDefaultFinder(Apple M3 Max,2026c).GetTimezoneNamesalways scans,
at about 8.8 µs.- The
xReaderAt route serializes queries internally to keep the query path
allocation-free, so its throughput does not scale with core count the way it
does for the byte-backed and expanded finders. - Command names in
cmd/still read*tzpb(geojson2tzpb,reducetzpb,
and so on) although no protobuf remains in the pipeline; they are kept so
the existing data-build workflows keep working.
Full Changelog: v1.2.5...v2.0.0