Changelog
9.0.0 (2026-09-10)
Breaking changes:
Importing
timezonefinderno longer binds its submodules as package attributes. Public names are resolved lazily; import a submodule explicitly before accessing it through the package.TimezoneFinderLand the shared base class no longer accept the ineffectivein_memoryargument.TimezoneFinder(in_memory=...)is unchanged.The command line requires an explicit
query,rows, orvalidate-datacommand. Replacetimezonefinder LNG LATwithtimezonefinder query LNG LATand--stdinwithtimezonefinder rows.
New features:
Added
timezone_ids_at()andtimezone_names_at()batch lookups to both finder classes and the global API. They accept separate one-dimensionallngsandlatsarrays and preserve input order. Solves issue #499.Added
timezone_ids_at_land()andtimezone_names_at_land()batch lookups for land-restricted results. Solves issue #499.Added
zone_names_from_ids()for converting arrays of zone IDs after filtering, joining, or grouping them. Solves issue #499.Added the exported
NO_ZONE_IDsentinel used by batch ID lookups whenon_invalid="skip"leaves an invalid coordinate in place. ID-taking methods reject the sentinel instead of treating it as a negative index. Solves issue #499.Batch lookups accept
on_invalid="skip"to preserve valid results while marking invalid coordinates; the default continues to raise. Non-numeric inputs raiseTypeError. Solves issue #499.Added
zoneinfo_at()to both finder classes and the global API, returning a standard-libraryzoneinfo.ZoneInfo. Solves issue #502.Added
utc_offset_at()to both finder classes and the global API. Solves issue #502.Added
localize()to both finder classes and the global API for attaching the coordinate’s local timezone to a naive datetime. Solves issue #502.Added
timezonefinder validate-data DIRfor exhaustive, opt-in validation of custom compiled datasets. It reports incomplete and inconsistent files as validation errors and never runs on finder construction. Solves issue #500.
Changed:
NumPy arrays retained as dataset state are read-only, so mutating publicly reachable implementation arrays fails instead of corrupting later lookups.
timezonefindercan now expose its documented public surface without importing NumPy, H3, or the polygon readers until a finder-related name is first accessed.Candidate polygons are ordered by the work needed for a full lookup, including hole checks.
TimezoneFinderLtherefore returns the final fallback of that ordering, not an estimate of the zone covering most of the H3 cell.The revised shortcut ordering ships in
timezonefinder-data3.2026.3.post1, a rebuilt output from upstream 2026c rather than a falsely labelled 2026d release.The root dependency now requires
timezonefinder-data>=3.2026.3.post1,<4so installations cannot retain older format-3 data that lacks the ordering promised by this release.Polygon coordinates use block-packed residuals on the upstream source grid instead of fixed-width absolute values. Custom compiled data directories must be regenerated with format generation 3.
The converter rejects upstream geometry carrying precision beyond the declared source grid instead of silently rounding it.
Each boundary polygon carries a latitude block index that skips blocks whose edges cannot cross the query latitude.
Stored rings may begin at a different vertex because the converter rotates them to form tighter latitude blocks;
get_geometry()still returns the same closed path.get_geometry()andget_polygon()decode packed coordinates into returned arrays rather than returning views into stored coordinate buffers.Boundary and hole collections use the same packed geometry path in both memory modes. Solves issue #536.
The H3 shortcut index moves from resolution 3 to resolution 4. Benchmark fixtures classified as unique or ambiguous shortcuts are therefore not comparable with earlier releases.
The shortcut index uses a new array-based format with shared candidate lists and precomputed stopping positions. Custom compiled data directories must be regenerated.
The internal
shortcut_mappingattribute is removed with the dictionary-backed shortcut representation. Solves issue #477.Duplicate enclave geometry is stored once and referenced from holes through
holes/poly_ref.npy. A referenced hole returned byget_geometry()may start at another vertex or use the opposite winding while tracing the same path.The hybrid shortcut loader retains compact read-only candidate slices rather than keeping the complete serialized shortcut buffer alive.
certain_timezone_at()andget_geometry()load the zone-position table lazily once per finder instead of reopening it on every call.Linux wheels now target
musllinux_1_2instead of the retiredmusllinux_1_1image.
Bug fixes:
TimezoneFinder.cleanup()and leaving a finder context now release the memory-mapped coordinate files before returning, including when an operation raises.Geometry-backed operations after
cleanup()or after leaving a finder context now fail instead of continuing through mappings that should have been released.Public methods accepting zone or boundary IDs now reject negative values with
ValueErrorinstead of indexing from the end of the dataset.coord2intno longer wraps out-of-range values silently when Numba is installed.Source vertices are rounded to timezone-boundary-builder’s six-decimal grid rather than truncated toward zero.
get_geometry()therefore returns corrected source-aligned coordinates.Shortcut compilation detects boundary edges that cross an H3 cell without either shape containing a corner, fixing missed candidates near the Lesotho border, the Strait of Malacca, and the poles.
Shortcut compilation retains a polygon when an H3 cell extends outside one of its holes despite all cell corners lying inside the hole, fixing wrong answers around Pacific atolls.
Shortcut compilation handles cells crossing the antimeridian without treating wrapped longitude bounds as empty.
The point-in-polygon C extension uses fixed-width arithmetic for slope products and no longer overflows on Windows.
pip install timezonefinder[numba]no longer carries a stale upper bound on NumPy; Numba’s dependency metadata decides compatibility.
Documentation:
The README, usage guide, use cases, and bounded examples prefer context-managed finder lifetimes. They reserve explicit
cleanup()for long-running services and workers that reuse one finder until orderly shutdown.Reworked the aware-datetime and UTC-offset examples around the standard-library helpers; Windows users must install
tzdata. The retainedpytzexample useslocalize()rather thanreplace(tzinfo=...). Solves issue #502.The NumPy 1 migration guidance identifies 8.2.1 as the last compatible timezonefinder release and recommends an isolated environment when a system NumPy constraint conflicts.
The reduced
timezones-nowdocumentation reports the 2026c dataset accurately and points to the vendored upstream mapping for exact release-specific counts.The use-case documentation explains the inverted sign convention of
Etc/GMTnames and recommends resolving offsets throughZoneInfo. Solves issue #503. Thanks to weed33834 for PR #538.The usage documentation states that coordinates exactly on timezone boundaries have no guaranteed side and that
certain_timezone_at()may returnNonethere.The package comparison now states the border-correctness trade-off and directs readers to reproducible comparison reports rather than embedding transient performance claims.
Added a point-in-polygon acceleration-path report covering the C extension, Numba, and pure-Python implementations.
Internal:
Changelog fragments are filed under their final release-note section so assembly produces the grouped changelog before release curation.
Cleanup-warning tests isolate their synthetic destructor failure so later collection cannot leak a warning into the next parametrized case.
Added sampled shortcut-candidate coverage validation after dataset conversion, with replayable omission diagnostics.
Candidate polygon metadata is read through buffer views rather than repeated NumPy scalar indexing.
Hole membership first checks one union bounding box before inspecting individual holes.
is_valid_lat,is_valid_lng, andcoord2intare ordinary Python functions rather than one-operation Numba dispatches.Candidate loops read each polygon’s zone ID directly instead of gathering the full candidate list into a temporary NumPy array.
HoleArrayowns hole lookup, union bounds, and membership checks throughids_ofandany_contains.Hole ownership lookup returns an empty range directly rather than using a generator that catches
KeyError.FileCoordAccessor.cleanup()releases its own word view before closing the mapping; caller-held views may still defer the close.PolygonArraycaptures the point-in-polygon kernel alongside the buffers it serves, allowing different acceleration paths to coexist in one process.certain_timezone_atuses the shared shortcut candidate iterator instead of maintaining a second dispatch implementation.ShortcutIndexowns shortcut storage and decoding instead of exposing five arrays and duplicated bit arithmetic to finder methods.Shortcut slot addressing uses the contiguous H3 base-cell-and-digit bit field directly.
Scalar and batch lookup paths share one candidate-polygon loop.
Batch lookup preparation is memoized by shortcut entry rather than sorting coordinates by cell.
ShortcutIndex.entries_ofis the batch counterpart ofentry_ofand is checked against it across the complete shortcut index.Batch answer arrays use the narrowest signed dtype capable of representing every zone plus
NO_ZONE_ID; data validation refuses an overflowing zone count.Zone-ID-to-name conversion lives in
ZoneNamesbeside the table it reads;timezone_namesremains a public read-only list view.Batch coordinate admission lives beside scalar coordinate validation and preserves their intentional treatment of
None.Coordinate bounds are declared once in
timezonefinder.configsand reused by validators and diagnostics.get_last_change_idxlives beside the shortcut builder, its only caller, and compiled values are rechecked by data validation.Coordinate buffers are written with one NumPy vector copy using a dtype shared by writer and reader; values outside that dtype are rejected.
The converter creates paired boundary and hole output directories at their shared orchestration point.
Per-polygon zone IDs use the common vector writer.
ParseAccumulatorcarries converter collections and counters through the parse pipeline instead of ten parallel parameters.The converter’s zone-ID ordering invariant is enforced once and tested together with dtype, maximum-ID, and position-table rules.
Converter helpers no longer keep dead iterator, reassignment, or self-return constructs that resembled off-by-one errors.
Shortcut compilation annotations accept the actual list and array forms passed by production and tests.
Removed unused JSON and pickle helpers from
scripts.utils.Removed the unused
i8fallback fromtimezonefinder._numba_replacements.Removed an unused test helper that was retained only for possible future use.
Hex.poly_candidatesno longer hides an impossible uninitialized cache state behind an empty result.Hole polygons remain part of the compiled data because zone-level candidate precedence is cyclic and cannot preserve nested-enclave answers without them. Closes issue #513.
Data updates compare a frozen sample of answers with the committed baseline.
Data updates exceeding the answer-change threshold are prepared as draft pull requests for human review rather than discarded.
Data-update answer comparison runs only for the full-with-oceans dataset whose baseline it describes.
Boundary and hole payload sizes have separate release gates; polygon counts are recorded without gating renames.
Reduced-dataset expected names are translated through a vendored, digest-checked upstream merged-zone mapping. Solves issue #334.
Removed the unused hand-maintained reduced-zone mapping.
Upstream data downloads are checked against the release asset’s published size and SHA-256 before parsing.
Verified source identity is recorded in
DATA_SOURCEalongsideDATA_VERSIONonly after successful conversion.Upstream release API reads retry transient failures and ignore stale credentials for the public endpoint.
A branch declaring unpublished data can obtain a compiled wheel from
compile_data.ymlinstead of committing generated binaries.Packaged boundary binaries are git-ignored and obtained from the declared
timezonefinder-datawheel by local and CI bootstrap paths.DATA_BUILD_RUNbinds tests, benchmarks, builds, and publication to the same compiled data-wheel artifact.The data-wheel handoff accepts an artifact only when its packaged inputs match the checkout; otherwise it rebuilds from a published source.
Data publication uploads the wheel tested by its update pull request rather than an independent second build.
timezonefinder-datasupports consecutive PEP 440.postNreleases for rebuilt output from the same upstream boundary release.The data package README records a concise summary for every data release, including 3.2026.3.
The code-release guard verifies that the built wheel’s compatible data requirement is served by PyPI before creating the GitHub Release.
The PyPI publishing job declares both OIDC and checkout permissions.
timezonefinderuses PyPI Trusted Publishing instead of a long-lived API token. Solves issue #525.Artifact staging is shared by one composite action, with data wheels excluded from the code publisher’s staging directory.
Release tags reuse the successful
mastertest matrix instead of running it a second time.Tag publication verifies a successful
buildrun for the exact commit onmaster.Packaging checks validate generated wheel and sdist metadata as well as repository-backed essential files.
Packaging exclusion patterns must match real checkout paths and remain aligned with
MANIFEST.in.The test build commands target the interpreter running the test suite.
Release workflow tests classify every script-side guard by the irreversible action it must precede.
Pull requests use a template covering the change, motivation, and verification.
Weekly data updates resolve and download one explicit upstream release instead of racing separate “latest” requests.
Weekly data updates regenerate, open, merge, and publish through distinct data-release workflows.
update_data.shuses non-interactive dataset flags and writes data-release notes automatically.Changelog changes are filed as independently mergeable fragments and assembled during a release.
The build workflow exposes one stable
ci-okaggregate gate that fails when any required job is skipped, cancelled, or unsuccessful.Release-branch pushes render commit-bound benchmark report artifacts automatically.
Published benchmark pages are rendered on named CI runners and distinguish full-suite reports from the tracked core subset.
Benchmark and memory commands assert the acceleration path they intend to measure.
Benchmark reports are generated from isolated plain-install environments rather than the contributor’s optional dependency set.
CI benchmark comparisons report lookup throughput and memory comments use the median statistic.
The trend chart uses human-readable labels and timezone lookups per second rather than batch iterations per second.
CI tracks both batch lookup forms across random, unique-shortcut, and ambiguous-shortcut workloads.
Pull-request benchmark comments are replaced by posting the new commit-bound result before deleting the superseded comment.
Pull-request benchmark runs are path-filtered to changes that can affect tracked measurements.
Added a reusable, order-alternating candidate-comparison harness with unresolved outcomes when its estimators disagree.
Added per-query latency reporting alongside batch-oriented pytest-benchmark reports.
Added block-filtered point-in-polygon benchmarks for the kernel path used by real lookups.
Added a reproducible comparison suite against
tzfpythat records the compared package version.Added a memory harness that records allocation and resident-set metrics separately.
Added a query-stage profiling prototype and recorded its scope and limitations in contributor documentation. Solves issue #497.
Added a whole-query shortcut-resolution prototype whose correctness gate exercises alternative index resolutions.
The single-resolution prototype prices the serialized layout actually shipped and includes resolution 5.
scripts/tune_block_size.pyvalidates candidate block sizes against rotated rings and the packaged layout.Benchmark names and memory metric names are pinned so chart-history joins cannot drift silently.
Generated benchmark reports open with their measurement configuration and direct readers to methodology details.
Hand-written documentation links to generated reports rather than copying changing measurements.
The benchmark methodology documents paired candidate comparison and the asymmetric interpretation of stage shares.
The data-report generator returns RST and accepts an explicit output path instead of relying on redirected stdout.
The benchmark-report generator returns RST and accepts an explicit output path instead of relying on redirected stdout.
Parsing custom output writes its data report beside those binaries; only parsing the packaged directory updates the committed report.
The data report derives the H3 cell count from H3 rather than a finite table with a misleading fallback.
Shortcut-index report statistics are calculated by four typed, independently testable helpers.
The data report labels directly resolved cells as requiring no polygon tests rather than as storing zero polygons.
Report renderers choose labels and prose through named branches and shared parameter labels.
Coordinate resolution and ground-distance helpers derive documented precision from the dtype and scale.
Data-format documentation distinguishes source precision from storage capacity and explains accepted floating-point inputs.
Data-format documentation records why shortcut cells never make boundary geometry redundant.
The package comparison states when
tzfpyis the more appropriate choice and links measurements to their source.timezonefinder.flatbuf.schemas.__all__contains only attributes the module defines.Ruff enforces
flake8-bugbearandRUF013consistently in direct and pre-commit runs.Warning calls identify their caller through
stacklevel=2.Ruff’s prototype exclusions are declared once.
Ruff rule
B023is enforced for functions that close over loop variables.Ruff rule
B905is enforced, with explicit non-strictzipcalls where variable arity is intentional.Mypy covers
tests/and the hook installs the stubs those annotations require.Optional-dependency import failures explain the stale environment and recommend
make install.make force_updatesynchronizes all dependency groups.make outdatedcorrectly parses tree-formatted package names.Pytest class-scoped fixtures use the classmethod form required by future pytest releases.
Cleanup exception tests are parametrized over the complete suppressed and warned exception sets.
Cleanup test doubles fail once so natural finalization cannot leak warnings into a later parametrized case.
CLI tests select application diagnostics by prefix instead of counting unrelated interpreter stderr lines.
Shared test helpers derive the repository root from
scripts.configsso wheel installs do not resolve it insidesite-packages.
8.3.0 (2026-08-19)
New features:
The dataset version is now exposed at runtime.
TimezoneFinder().data_version(andTimezoneFinderL().data_version) return the timezone-boundary-builder release the packaged data was built from, read from adata_version.txtstamp thatscripts/file_converter.pywrites into the data directory it generates and that ships in the wheel. Previously an installedtimezonefindercould not state it at all: the release tag lived only in a repo-root file that is not packaged. Which release a parse is stamped with comes from the input’s filename (combined-with-oceans-2026c.json, whichupdate_data.shnow produces), or fromscripts/file_converter.py --data-versionfor an input that cannot carry it; your own GeoJSON is stamped"unknown", and an unpacked release archive that lost its tag is refused rather than compiled into data that could never say where it came from.timezonefinder.__version__is now exposed as well, read from the installed distribution metadata. Solves issue #498.The command line script gained a
--stdinstreaming mode: it reads delimited rows from standard input and writes each back out with atimezonecolumn appended, building the finder once instead of paying full initialisation per coordinate. Which columns hold the coordinates is read off the header by name, or stated with--lng-col/--lat-col, and never inferred from their position - a swapped pair is still a valid coordinate for any longitude between -90 and 90, so guessing would answer with a real but wrong timezone instead of failing. Every input row produces exactly one output row, and a row that cannot be used warns on stderr and makes the run exit non-zero rather than ending the stream. Whether the first row is a header is worked out from the row, or stated with--header/--no-header. New flags-d/--delimiterand--in-memoryapply to the whole stream. See the usage documentation. Solves issue #504. Thanks to weed33834 for the PR #516.
Changed:
Polygon coordinates are now stored one axis at a time in the packaged
coordinates.binfiles - all x values followed by all y values per polygon, instead of interleaved. The point in polygon test scans a single axis per iteration, so contiguous per-axis blocks halve the cache lines it touches: ~1.6x faster on a median polygon and ~2.5x faster on the largest ones via the C extension, 14-25% faster via Numba. The bundled data was regenerated accordingly, and the layout is described in the data format documentation.Every packaged FlatBuffers file now carries a file identifier and a layout version, and
TimezoneFinderraises aValueErrornaming the offending file when either does not match - previously such a directory was read without complaint and produced wrong timezones. Forcoordinates.binthe version records how the coordinates are encoded and which polygons the file holds. The hybrid shortcut binaries get an identifier that differs per zone id width, because the uint8 and uint16 schemas differ only in the width of a zone id and each parses cleanly as the other; the width is now read from the buffer instead of being guessed from the file name, so a renamed or mispaired shortcut file fails loudly rather than returning wrong zones. If you compile your own data and pointbin_file_locationat it, regenerate it once withscripts/file_converter.py, since the coordinate layout, the hole storage, the shortcut container and the file names all changed in this release. The markers track what a file holds rather than the package version, so this is not a per-release obligation. Solves issue #458.The hybrid shortcut loader no longer keeps the entire shortcut binary in memory. The polygon id arrays it returns were zero-copy views onto the ~1.5 MB file buffer, so ~47 KB of live data pinned the whole thing for the lifetime of every
TimezoneFinder/TimezoneFinderLinstance. They are now disjoint read-only slices of a single compact array, cutting the shortcut mapping’s footprint from ~7.4 MB to ~4.7 MB per instance, and every finder’s resident set by ~2 MB, at unchanged initialisation time - which matters most for concurrent workloads, where the recommended one-instance-per-thread pattern multiplied the waste.Holes that duplicate a timezone boundary polygon are no longer stored twice. Almost every hole is an enclave, cut into the surrounding zone with exactly the ring the upstream data also emits as the enclosed zone’s own boundary polygon - the same geometry under two IDs. The packaged hole coordinate file now holds only the rings with no such twin (27 of 756 in the current data), and a new
holes/poly_ref.npyrecords per hole which boundary polygon to read instead. Hole data drops from ~2.0 MiB to ~0.16 MiB, andin_memory=Truesaves the same amount of RAM, since those holes now resolve into the boundary arrays rather than materialising a second copy. Matching is exact - rings are compared as integer coordinates in a canonical form, with bounding boxes used only to narrow the search - so every timezone lookup returns what it did before. One visible consequence:get_geometry()may hand back a deduplicated hole ring starting at a different vertex or winding the other way than it used to, tracing the same closed path. The encoding is described in the data format documentation.The timezone boundary data now ships as its own distribution,
timezonefinder-data.pip install timezonefinderis unchanged - it is a hard dependency and is installed automatically - but the dataset can now be pinned on its own (pip install timezonefinder "timezonefinder-data==1.2026.3"), where previously holding a dataset meant pinning an oldtimezonefinderand forfeiting every code fix since. Every release used to carry the whole ~65 MB dataset in three platform wheels plus an sdist to distinguish a few kilobytes of compiled code, which had already exhausted the PyPI project storage quota once. A data update is consequently no longer atimezonefinderrelease at all: it publishestimezonefinder-dataunder its own tag namespace and is recorded in that package’s README rather than here. Its version reads<format>.<year>.<letter>-1.2026.3is data format generation 1 built from timezone-boundary-builder2026c- andtimezonefinderrequirestimezonefinder-data>=…,<2: no ceiling on the data axis, so a dataset update needs no code release, and a hard one on the format axis, so code paired with data it cannot read fails when resolving rather than at the first lookup.DATA_LICENSEmoves with the database it covers and now ships inside the data wheel, and a compiled data directory additionally carries aschemas/copy of the FlatBuffers definitions its binaries were written by, so it can be read back without the package that wrote it. Solves the first part of issue #446.The packaged FlatBuffers binaries are now named
.binrather than.fbs:boundaries/coordinates.bin,holes/coordinates.binandhybrid_shortcuts_uint16.bin..fbsis the FlatBuffers schema extension, and the data directory now ships actual schemas next to the buffers, so one extension was naming two unrelated kinds of file. Each buffer already states what it is through the file identifier in its first bytes, which is what a rename or a mispaired copy cannot forge - the name never carried that meaning. The bytes are unchanged.
Bug fixes:
Fixed a
BufferError: cannot close exported pointers existraised during resource cleanup in file mode (in_memory=False). A coordinate array obtained fromcoords_of()is a zero-copy view onto the memory-mapped file, andmmap.close()refuses to unmap while one is alive, so an array outliving itsTimezoneFinderraised on cleanup. The mapping now stays valid instead of leaving the views dangling, andFileCoordAccessor.cleanup()releases its own references so the deferred close happens as soon as the last view is dropped. The accessor must not be used aftercleanup().
Documentation:
The memory footprint of every finder configuration is now measured and published in a new memory report, separating what a configuration allocates (
tracemalloc) from what it makes resident (RSS, which additionally counts memory-mapped pages). The distinction is the point: the default mode maps the coordinate data instead of reading it, so it allocates an order of magnitude less than the in-memory mode, and only the pages a lookup actually touches become resident. This replaces documentation claiming a 40MB process ceiling and a 41MB data directory, both long out of date.Restructured the two entry points a reader actually arrives at -
README.rstand the documentation landing page - so both state what the package is and how it works instead of only what it is called. The README opens with the project banner and a one-sentence statement of what the package is for, then the badges, then the quick guide - and adds three short sections that were missing entirely: How it works (the lookup pipeline and the no-simplification trade-off), Performance (a concrete throughput figure with its configuration named, the three point-in-polygon backends and the pure-Python fallback), and Engineering notes linking the architecture, data format and benchmarking methodology pages. The maintainers-wanted notice moves from the first heading after the intro into a newContributingsection at the bottom, which also linksCONTRIBUTING.mdfor the first time. The badge block is corrected along the way: thecode style: blackbadge named a formatter this project has never used and is replaced byruff, and a supported-Python-versions badge was added. The banner is referenced by absolute URL, since PyPI serves the long description without the repository and adocs/…path renders as a broken image there. The landing page gains the same How it works summary, the no-simplification trade-off and the ocean-zone consequence fortimezone_at(), and its flat seventeen-entry table of contents is grouped into Using it, Design, Performance and Project, so the sidebar says what kind of project this is rather than listing pages in the order they were written.Rewrote the package comparison page. It now states its position in prose before the first table - border correctness is what this package optimises for, speed is the constraint that work happens under - and says plainly when
tzfpyis the better choice. Every quantitative cell names what it measures and links its source, and the speed row is deliberately qualitative on both sides, with a note explaining that the two packages have never been benchmarked under one harness. The decision table drops the rows on which the two packages do not differ.Two new documentation pages: Architecture describes the lookup pipeline, the three point-in-polygon backends and the memory modes, and states the ceilings this package deliberately does not exceed - unsimplified geometry, ~1 cm coordinate resolution, no general-purpose spatial code. It also documents how the package is built and shipped, which was previously described nowhere outside the workflow YAML: why one abi3 wheel per target replaces one wheel per Python version and what
abi3auditis guarding, why three libc targets are built, why the end-to-end job installs the built wheel and asserts the C extension loaded rather than merely importing the package, and why a tag pushed from outsidemasteraborts the release. The testing section gained the property-based suite and the reason the tox matrix is a matrix - the acceleration paths are bound at import time, so a passing run describes one configuration only. Both sections are linked from the README’s Engineering notes. Benchmarking Methodology documents how the published numbers are produced and what they can and cannot tell you:ubuntu-latestpins the runner image and not the CPU, which is why a pull request is measured against its own merge base on the same runner and why every alert threshold is derived from measured noise. It was previously addressed only to contributors, in the second half ofCONTRIBUTING.md, which now keeps the operational instructions and links to it.The H3 resolution choice in the data format documentation is no longer asserted to “offer a good balance” but reports the study behind it (
prototypes/single_resolution_bench.py): resolution 3 keeps the hybrid index at a small fraction of the packaged polygon data, while resolution 4 would exceed 10 % of it for gains that do not justify the increase.The hand-written documentation no longer restates exact figures that belong to the generated pages - dataset vertex, polygon and hole counts, index and distribution sizes, memory footprints, lookup throughput. Those change with every data update and with code that shifts a footprint, which silently left the copies wrong: the memory figures had already gone stale in four places. The prose now states the magnitude that survives a data update and links the data report or the relevant benchmark report, which are regenerated from the packaged data and are always current.
The three weakest hand-written documentation pages no longer answer a question by pointing at a file the reader has to open. The performance page now opens with the four benchmark reports and the trend chart instead of a bullet list of adjectives about the binary format, and its C extension and Numba sections are cut to what a user does - which call reports the active backend - with the explanation left to the architecture page that already carried a more precise version of it. Getting started lists the four runtime dependencies and what each is for, where it previously said to consult
pyproject.toml, which remains linked as the authoritative source for version ranges. The use case pages carry runnable snippets for building an awaredatetimeand reading a UTC offset, with theexamples/scripts as the follow-up rather than the whole answer; the snippets use the standard library’szoneinfo, so neither needs an optional dependency.The shortcut entry distributions in the data report no longer report three quarters of all H3 cells as holding
0polygons, which is impossible for data whose ocean zones cover the globe. Those cells are covered by a single timezone and store its id directly, so a lookup there needs no point-in-polygon test at all - the column is now Polygons to test and the row reads none (unique zone). The tables are introduced by a sentence on what they measure, including why no cell ever needs exactly one test.The usage examples in
README.rstand the usage documentation now show the result the packaged data actually returns. Every snippet queries the same Berlin coordinates and annotated the answer as'Europe/Paris', which is the value from the reducedtimezones-nowdataset, whereEurope/Berlinis merged intoEurope/Paris- not from the full dataset the package ships by default. All eleven annotations now read'Europe/Berlin', verified against the packaged data for each oftimezone_at(),timezone_at_land(),certain_timezone_at(),unique_timezone_at()andTimezoneFinderL, and theget_geometry()call in the opening example asks for that same zone instead of a different one.tests/test_documented_contracts.pynow re-runs each of those documented lookups, so a data update that moves the example coordinate’s zone fails there rather than leaving every snippet on both pages quietly wrong again.
Internal:
The release pipeline refuses to publish
timezonefinderunless a compatibletimezonefinder-dataalready exists on PyPI. The two distributions release independently, and on a data format change the order is fixed - data first, then the code requiring it - because a code wheel whose declared data version does not exist yet is uninstallable for everyone until it does, and the version number cannot be reused to fix it. The check reads the requirement out of the built wheel rather than out ofpyproject.toml, and asks the index the same question a user’s resolver will, so a yanked release does not count as one that satisfies it. It runs before the GitHub Release, which is the first step of the release that cannot be taken back.Pull requests are opened against a template (
.github/pull_request_template.md) prompting for the change, its motivation and the checks that were run.update_data.shresolves the timezone-boundary-builder release tag before downloading and fetches that release’s asset, instead of fetchingreleases/latest/download/and separately asking the API whatlatestwas - two independent questions that a release landing between them answered differently, attributing one release’s data to the other. The tag now names the downloaded archive and the GeoJSON as well, so a leftover file from another release or another dataset variant cannot satisfy the “already downloaded” checks and be parsed in place of what was asked for.Names and docstrings now describe what the code does.
TimezoneFinder.timezone_atdocuments the optimisation it actually performs: once no other zone can be matched the last remaining zone is returned without a point in polygon test, which is always correct against the packaged data - the ocean zones cover the globe, so every point lies within one of the candidate polygons - but not against custom data that leaves areas uncovered, where a point inside none of the candidates is still attributed to that zone andcertain_timezone_atis the method that tests every candidate. Three tests were named after something other than what they do:test_rectify_coords_valid/_invalidwere named for arectify_coordsthat exists nowhere in the package and both callvalidate_coordinates, and the first was subsumed entirely bytest_validate_coordinates_accepts_finite_values, which covers all four of its distinct corners and additionally asserts the return value where the older test asserted only “does not raise”; andtest_single_element_arrays_should_not_occurasserted that they do occur (assert single_element_count == 2) under a triple-quoted string placed after the first statement, making it a discarded expression rather than a docstring - so it reached neither--collect-onlynor a failure report, which is where the contradicting name was the only thing a reader saw. A stale comment duplicated across the last two lines oftests/main_test.py, reading as a to-do for somethingTestTimezonefinderClassTestMEMalready does, is gone.Added
DATA_VERSIONfile tracking which timezone-boundary-builder release the packaged data was generated from, written automatically by the data update script after a successful parse. Thanks to Lucas Hemkemeier for the PR #429.The packaged data now updates itself: a weekly workflow compares
DATA_VERSIONagainst the latest timezone-boundary-builder release, regenerates the data and opens a ready-to-review update PR, which is merged and released automatically once its CI passes - the version tag is pushed with a GitHub App token, since the default one would not trigger the release pipeline. The tag lives in its owndata-v*namespace, which the code release pipeline excludes at its trigger and again on the job that creates the GitHub Release, and the data stream publishes by PyPI Trusted Publishing from its own deployment environment rather than with a shared token. It refuses to release when the squash it produced did not land on themasterit checked, so the tag names a tree that was actually built. Failed CI takes the same manual-attention path and falls back to the previous notification issue. Each cause labels the PRautomation-failedand leaves one comment naming that cause and linking the run, deduplicated per cause so re-running CI neither repeats a notice nor hides a second one; a failure past the merge is a cause of its own, since that leaves master carrying the update with no tag pushed and only a hand-pushed tag still releases it. The manual release path drops the stop condition it carried for an out-of-orderCHANGELOG.rst: the automation can no longer produce one, and the test suite asserts the committed file’s section order if anything else does (issues #273, #167 and #510). Thanks to Lucas Hemkemeier for the PRs #434 and #436, and to Nice6042 for the PR #518.update_data.sh(renamed fromparse_data.sh) is CI-ready: interactive prompts replaced by flags (--dataset=full|same-since-now,--with-oceans,--rm-tmp), the release note for a data update written automatically into the data package’s README, no redundanttoxrun, and amake reportsat the end so the benchmark and data reports cannot go stale relative to the data an update PR ships. A standalonemake parse/make testparsestill needs a manualmake reports(issues #167 and #510). Thanks to Lucas Hemkemeier for the PRs #432 and #434.Added property-based tests (
hypothesis) for coordinate validation (solves issue #143). Thanks to Lu Yicheng for the PRs #431 and #433.Replaced the hand-rolled
timeittiming inscripts/check_speed_*.pywithpytest-benchmarksuites underbenchmarks/, excluded frommake test/make testallviatestpaths. Both they and the memory harness run over deterministic committed fixtures (tests/fixtures/benchmarks/), so two runs of the same commit execute the exact same workload; the loader rejects fixtures that no longer match the checkout. Measurement and rendering are decoupled, sodocs/benchmark_results_*.rstcan be regenerated from a stored JSON without re-measuring. Run viamake speedtest,make benchmarks,make memoryormake reports.Memory is measured by its own harness (
scripts/measure_memory.py,make memory) rather than bypytest-benchmark, which times code and would have its timings distorted by allocation tracking. It emits pytest-benchmark-shaped JSON, so the existing normalisation, noise and comparison tooling works on it unchanged given a--metric.tests/test_memory_footprint.pyfails if a mode’s allocation leaves its order of magnitude - the regression that would makein_memory=Falsestop being the low-memory option.Added continuous benchmarking on CI (solves issue #150), deliberately kept out of the release pipeline in
build.yml: the tracked core subset and the memory harness run on every pull request and every push tomaster, publishing trend charts togh-pagesand posting a same-runner base/head comparison on the pull request. The comparison reports added or removed benchmark IDs without trying to ratio unmatched measurements, which lets the trusted default-branch consumer remain compatible while the suite evolves. A pull request is measured against its own merge base in the same job rather than against a stored baseline, becauseruns-on: ubuntu-latestpins the runner image and not the CPU. The measurement design, the tracked estimator and every alert threshold are documented in the new benchmarking methodology page. The measuring job holds no write permissions and no secrets, so branch and fork pull requests behave identically; the comment is posted by a separate, privileged workflow viaworkflow_run.Guarded the benchmark plumbing against silent drift:
tests/test_benchmark_names.pyandtests/test_memory_metric_names.pypin the node ids and metric names that join a measurement to its chart history, so a rename fails loudly instead of starting an empty chart beside the orphaned old one;tests/test_benchmark_workflows.pyasserts that the constants duplicated across the two workflows agree, where a one-sided edit previously had no failure mode at all, and that the cross-machine trend chart cannot creep back into the pull request comparison; and every generated report page states the inputs it describes -docs/benchmark_results_*.rstthe fixture and timezone data versions they were measured against,docs/data_report.rstthe timezone data version its figures were derived from. Both stamps are covered by tests: one renders each report and fails if a renderer stops emitting it, another checks the committed pages against the current fixture metadata andDATA_VERSION, so regenerating fixtures or updating the data without re-rendering fails loudly instead of leaving a page whose numbers are all plausible and all stale.Every generator now emits output that is already pre-commit-clean, so regenerating and diffing compares like with like:
write_jsonsorts keys the waypretty-format-jsondoes, and neitherscripts/reporting.pynorBenchmarkReporteremits trailing whitespace on empty cells or a trailing blank line. Previously everymake parse/make reportsleft its outputs looking modified until the hooks had run, which masked whether a regeneration had actually changed anything.Every generated benchmark report now opens with its headline figure and the configuration behind it, above the tables: how long a lookup takes and how many per second, the per-check cost across polygon sizes, construction time, footprint per mode - all derived from the same parsed JSON as the tables, never hardcoded. The banner beneath states which acceleration path and platform produced the numbers, and says whether that is the configuration CI tracks: the committed reports are rendered from a developer machine with Numba enabled, while CI measures the C extension without Numba, so their figures were never comparable to the trend chart and now say so.
make flatbufno longer overwrites hand-maintained__init__.pyfiles.flatcderives its output path from the schema namespace and writes an empty__init__.pyat every level of it, so generating in place wiped the__all__intimezonefinder/__init__.py- the whole public API. The target now generates into a scratch tree, copies back only the generated packages, and runs the formatters on the result so a regeneration diff shows the codegen change rather than formatting churn.Mypy now type-checks the whole package except the
flatc-generated bindings.ignore_errorspreviously covered roughly 800 lines of hand-written code as well, where a blatantly wrong return type still reported “Success”; they all pass once the exemption is lifted, bar two genuine findings now fixed.tests/test_mypy_config.pykeeps the list restricted to generated code, so silencing a module is a reviewed decision rather than a one-line edit.The hybrid shortcut reader and writer now select their FlatBuffers schema from a single registry (
SHORTCUT_SCHEMASintimezonefinder/flatbuf/io/hybrid_shortcuts.py) instead of dispatching on the zone id width in three places, each keyed differently. OneShortcutSchemaper width owns the width, the file name, theuintNmarker and the maximum zone id, which were previously written down across five places with nothing tying them together. Verified behaviour-preserving down to the bytes: re-writing the shipped shortcut binary produces a byte-identical file.Each distribution’s build is now asserted to contain exactly what it should. The data wheel’s payload is compared against the committed dataset as a set, in both directions: a missing binary fails on first use and gets reported, but an extra one ships silently - setuptools copies package data into
build/liband never prunes it, so a file renamed in the source tree keeps being zipped into every later wheel built from that checkout, which is how a 63 MBcoordinates.fbswas still shipping next to thecoordinates.binthat replaced it and doubling the wheel whose size is the reason the distribution was split out. The wheel builders clear that directory first, so a local build matches the fresh checkout CI builds from, and the code sdist’s checks cover its grafted test fixtures again.The packaged data is additionally held to a floor on how much hole deduplication achieves - the test suite fails if fewer than 90% of its holes match a boundary polygon (96.4% currently), because a future upstream release that stopped emitting enclaves as shared rings would still compile and still return correct timezones, just with the shipped data quietly re-inflated. The floor applies to that dataset and nothing else: compiling your own GeoJSON with
scripts/file_converter.pyis a supported use case, holes that are ordinary interior rings rather than enclaves are stored inline and answer correctly, and the converter only reports the ratio rather than refusing to compile.prototypes/hole_boundary_redundancy.pyis the study behind the threshold: it reads the upstream GeoJSON, so re-running it against a new release re-verifies the assumption rather than restating it.prototypes/hole_removal_impact.pyis the study behind keeping the unmatched holes stored inline rather than dropping them, which is the obvious next step and does not work: dropping holes and re-running the lookups changes answers, wrongly, because being covered by another zone only puts that zone among the shortcut candidates and says nothing about it being tested first (issue #513).Removed constructs that provably did nothing, and gave two vacuous tests real assertions. Most consequentially, four
__slots__entries were declared but assigned by nothing, which silently re-permitted the very attributes__slots__is there to forbid - assigning those names now raisesAttributeError, andtest_declared_slots_are_assignedkeeps the list honest.get_corrected_hex_boundariesexists once again. An earlier refactor left two verbatim copies of the antimeridian and pole clipping rules with nothing keeping them in sync; the copy without callers is deleted, and the survivor is now covered bytests/hex_utils_test.py- it previously had no direct tests at all.scripts/configs.pyno longer declaresMAX_LAT/MAX_LNGas a second pair of names fortimezonefinder.configs’s constants.prototypes/has aREADME.mdsaying what the three scripts there are: exploratory studies behind committed design decisions, run by hand, outside the package and the test suite. One of them is the measurement that chose H3 resolution 3 - the central algorithmic parameter of the package, already cited from the data format page - and another is the evidence for not building a hierarchical index.MANIFEST.innow excludes the whole directory from the source distribution rather than only its*.pyfiles.plans/is git-ignored alongsidetmp/and.venv/: implementation plans written while working on a change are local scratch, and leaving the directory untracked-but-unignored made it noise in everygit statusand a candidate for an over-broadgit add.Failing paths now report the input that failed.
tests/auxiliaries.py’srun_commandassembled the child’s stdout and stderr into a message and then raised a freshCalledProcessErrorthat never used it, withfrom Nonediscarding the original too, so a packaging failure undermake testintreported an exit code and nothing about the cause; it now echoes the captured streams and re-raises the original exception with its traceback intact.scripts/reporting.pypasses the coordinate file paths intoget_polygon_collection, whose optionalfile_pathexists precisely so an incompatible-layoutValueErrorcan say which of the two files was stale -make reportsagainst an outdated data directory previously could not.Boundaries.overlapsnames the type it rejected instead of raising a bareTypeError, and theRuntimeErrorfor missingoriginal_polygonsnames the polygon and resolution it was computing. The two re-raises ruff flags underB904now sayfrom Noneexplicitly, so a deliberately dropped exception chain is distinguishable from a forgotten one, andtimezonefinder/command_line.pydropsFileNotFoundErrorfrom anexcepttuple that already caught its base classOSError.tests/test_error_diagnostics.pypins what each of these messages must contain.The command line interface no longer routes its own output through a temporary file.
mainredirected stdout to amkstempfile for the duration of the lookup and then, in verbose mode, reopened it to read back a string it still held in a local variable - nothing inside the redirected block ever wrote to stdout, since the lookup functions return their result rather than printing it. The context manager, the read-back, its warning path and the file cleanup are gone, and the lookup function is now resolved once per invocation instead of twice, so-f 3/-f 4under-vno longer construct a secondTimezoneFinderLand reload its shortcut data just to read a function name. Output is unchanged character for character, across every function id in both modes.tests/cli_test.pygains the coverage that makes that checkable - verbose mode, the empty line printed when no timezone is found, and the rejected function id had none - and asserts the printed name verbatim instead of passing it throughrstrip("\n\x1b[0m"), which strips a set of characters rather than a suffix and so truncates 12 of the packaged zone names (Europe/Amsterdam->Europe/Amsterda).Docstrings now describe the code that exists. Six documented something the implementation contradicts:
AbstractTimezoneFinder.__init__calledin_memoryinert and “kept for API compatibility” when it is what selects memory-mapped against in-memory coordinate access - the claimhelp(TimezoneFinder)surfaces, and the opposite of what the usage docs say; bothget_geometrydocstrings pointed at atimezone_names.jsonthat does not exist under that name;read_zone_namespromised an empty list where it raisesFileNotFoundError, and illustrated itself with a hardcoded zone count that the packaged data had since outgrown; andzone_id_of/zone_name_from_ideach advertised an exception type they convert away, sending callers to write handlers that can never fire. Five further:param:/Args:entries inscripts/andtests/documented arguments that were removed along with the parallel shortcut compilation they belonged to.tests/test_documented_contracts.pypins the exception types and the coordinate access mode, so those promises now rest on something besides prose.The test and benchmark suites no longer contain checks that cannot fail. Eighteen calls sat inside four shared
pytest.raisesblocks intests/main_test.py, and execution leaves such a block at the first statement to raise - so one out-of-range coordinate, one positional call shape and one rejectedget_geometryinput were verified while the remaining fifteen were unreachable. Each is a test case of its own now: every coordinate just outside the WGS84 range, every positional call shape of every keyword-only lookup method, and the unknown-zone-name, past-the-end and negative zone id rejections ofget_geometry. The__del__cleanup test binds its exception per iteration rather than closing over the loop variable, which decided what a garbage-collected instance would raise long after the loop had moved on. On the benchmark side,pip_inputs_by_stratumvalidated only the strata the fixture happened to contain, so one missing from it altogether passed and surfaced later as a bareKeyErrorinside a benchmark, and the points and their labels were paired from two files with a non-strictzipthat truncates silently. That grouping now lives intests/auxiliaries.pyasgroup_pip_inputs_by_stratum, checks against the declaredPIP_STRATA- which the generator no longer keeps a second copy of - and has tests for each way the two fixture files can disagree.Both point-in-polygon acceleration paths are now covered by a local test run, whichever one the environment happens to bind. The implementation is selected at import time and Numba wins whenever it is importable - which the documented setup (
uv sync --all-groups) makes it - so the C extension was reached only by direct-kernel tests on hand-built arrays, and everything about how real polygon buffers arrive at it, including the read-only memory-mapped views, was first exercised in CI’s non-numba tox environments: the configuration a plainpip install timezonefinderproduces.tests/test_acceleration_paths.pynow rebindsutils.inside_polygonand drives the full lookup stack through both implementations, asserting that they agree across the real boundary data, that the C path returns the known-correct answers, and that the point-in-polygon stage was reached at all rather than short-circuited by the shortcut layer (issue #482).The packaging guard in
tests/test_package_contents.pyno longer names files that do not exist. It asserts that nothing in the built sdist and wheel matches a list of unwanted paths, which passes just as readily when a pattern matches nothing at all:.githublacked the trailing slash that directory patterns need,Agents.*stopped matching when the file was renamed toAGENTS.md, andreadthedocs.yamlnever matchedreadthedocs.yml- so the CI configuration and both of those files were unguarded while the suite stayed green. The patterns are corrected, the provider stubs,contributing/,.agents/,.claude/and.cursor/are covered to match whatMANIFEST.inexcludes, andtest_every_unwanted_pattern_matches_a_project_filenow fails on any hand-written pattern that matches no path in the checkout, so the next rename cannot silently disarm one. It carries theunitmarker rather than the module’s former blanketintegrationmark, since it needs no build: a mistyped pattern surfaces inmake test..gitignorere-include lines (!…) are also no longer read as exclusions, which had produced one more parametrised case that could never fail. The converse direction is checked too:test_every_manifest_exclusion_is_guardedparses theexclude/recursive-exclude/prune/global-excludedirectives out ofMANIFEST.inand fails when one of them keeps a path out of the build that no pattern here names - previously such a line was enforced by the build and verified by nothing, so deleting it would have shipped the file with the suite still green. The two lists are hand-maintained statements of one intent and had drifted before, in both directions. The architecture page describes the guard from both sides: among the tests that exist to give an invariant a failure mode, and under How it ships as the check on what the built artifacts actually contain.The distributions built by the test suite are now built for the interpreter running it.
uv buildwas invoked without--python, so it targeted the newest interpreter on the machine, whiletests/test_integration.pycreates its throwaway venv fromsys.executable: on a checkout whose.venvis older than the newest installed Python,make testintproduced acp314wheel and failed with pip’s “not a supported wheel on this platform”. Every tox environment offers a single interpreter, so the two agreed by accident in CI and the mismatch only ever hit developer machines, where the workaround was to pinUV_PYTHON.test_build_commands_pin_the_running_interpreterkeeps the pin in place; it needs no build, so it fails inmake testrather than waiting on a CI environment that cannot reproduce the mismatch.Two tests no longer leak numpy’s global error state into whatever pytest collects next.
np.seterrand the warning filters are process-global, andtest_overflow(tests/main_test.py) plustest_inside_polygon(tests/utils_test.py, six parametrisations) each set them and never restored them - so every later test in the same process ran withunderpromoted fromignoretowarn, and which of the two modules pytest collected first decided the state the other ran under. The filters were undone only incidentally, by pytest’s per-testcatch_warnings(), not by the tests themselves.benchmarks/conftest.pyalready had the correct pattern; it now lives intests/auxiliaries.pyas thestrict_numpy_errorscontext manager plus a thinstrict_numpy_warningsfixture, re-exported through the conftest of each suite, and both call sites request it. The context manager form is what makes the restore directly testable - a leaked global otherwise surfaces only as an unrelated later failure that depends on collection order, which is the hardest kind to attribute.The zone id invariants in
scripts/timezone_data.pyare each enforced in exactly one place, and now have tests.ZoneCollection.validate_structureandzone_positionseach walkedpoly_zone_idselement by element checking it was non-decreasing and each raised the same message built from its own locals; the scan moves into one_validate_non_decreasinghelper andzone_positionsdrops its copy, which could only ever have fired if a caller mutated the array in place - the validator runs at construction and nothing writes to it afterwards. Aif min_zone_id < 0branch is deleted as unreachable: the same method rejects any non-unsigned dtype a dozen lines earlier, so it read as the guard against negative zone ids while being incapable of firing. The class had no tests at all, so what it actually promises - the unsigned-dtype rejection that makes a negative id unrepresentable, the ordering and maximum-id rules, and the shapezone_positionsreturns - is now pinned bytests/timezone_data_test.py.The seven out-of-range coordinates - one representable step outside the valid WGS84 range, per axis and at every corner - are declared once in
tests/locations.pyinstead of verbatim in bothtests/main_test.pyandtests/utils_test.py, where only one copy carried the comment explaining what makes them interesting and adding a corner to it left the other testing a smaller set.The shortcut compilation chain in
scripts/shortcuts.pyis annotated for what it is actually passed. Both annotations were the wrong way round:check_shortcut_sortingdeclarednp.ndarrayand only ever receives thelist[int]thatoptimise_shortcut_orderingreturns, and it hands thenp.ndarrayit derives tohas_coherent_sequences(lst: list[int]). Widened rather than swapped, sincetests/shortcut_test.pycalls the latter with real lists.The supported Python versions are declared in five places that cannot read each other -
requires-pythonand one classifier per minor version inpyproject.toml, thepy{...}factors oftox.ini’s envlist, the test matrix andCIBW_BUILD_VERSIONSinbuild.yml, andpy_limited_apiinsetup.py- and two “must match” comments said so while nothing enforced them.tests/test_python_version_support.pyfails when they drift, in either of the two directions that fail silently: a classifier added without a matrix entry ships a version the package claims to support and CI never runs, and arequires-pythonraised without moving the abi3 base builds wheels tagged for an interpreter that is no longer supported. Each assertion was checked against the specific one-sided edit it targets, and both comments now name the test.The data report generator states figures it derives rather than ones it restates, and its annotations describe what it returns.
calculate_shortcut_index_statstook the number of H3 cells existing at the shortcut resolution from a ladder of literals covering resolutions 0 to 4 and fell through, for anything else, to the number of cells actually stored - which reports coverage of exactly 100 % instead of failing - behind anexcept ImportErrorthat cannot fire, since h3 is a runtime dependency rather than an optional one. It asksh3.get_num_cells, which returns precisely the numbers that were tabulated. Running mypy overscripts/reporting.py, which the pre-commit hook excludes, found seventeen further disagreements between the module and its own signatures: the statistics bag was typed as holding scalars while returning two distributions,load_binary_data’s nine-key result was a baredictindexed by string literal, the table renderer declared string rows while stringifying whatever it is handed,mainwas annotatedNonewhile returning exit codes toexit(), andprint_polygon_distribution_tabledocumented a return value it never produced while its one caller discarded it. The two dict results are nowTypedDicts inscripts/configs.py, carrying tests that assert their keys against what is really returned, since CI cannot type-checkscripts/. The polygon count that labels a distribution row is no longer formatted into that label and parsed back out of it to key the example lookup.docs/data_report.rstand the benchmark reports regenerate byte-identically throughout.Removed five definitions nothing referenced - three JSON/pickle helpers in
scripts/utils.pyand theimport picklethey kept alive, thei8dtype shim intimezonefinder/_numba_replacements.pythat the no-numba fallback never imports, and a test helper self-documented as kept for future reference - and a guard inscripts/hex_utils.pythat could not fire.Hex.poly_candidatesre-read its cache after initialising it and returned an empty set if it were still unset, which no path through_init_candidatesleaves it: an empty set there means “no candidate polygons”, so a converter bug would have surfaced as silently missing shortcuts rather than as a failure. The property had no direct test, being reached only through shortcut generation, and now has one._memory_mode_labellooks its two labels up inPARAM_LABELSinstead of spelling them out, so renaming the display vocabulary can no longer leave the comparison bullets and the tables above them disagreeing.make parseandmake testparserun again. Both invokedscripts/file_converter.pyby path, which putsscripts/onsys.path[0]instead of the repository root, so the converter’s ownfrom scripts.timezone_data import ...raisedModuleNotFoundErrorbefore any work started - a total failure that CI never sees, since it runs neither target.make testparseis the only cheap end-to-end exercise of the converter (update_data.shneeds a ~55 MB download), and nothing undertests/coversparse_data(), so while it was broken the converter had no smoke test at all. The invocation documented in the usage docs had the same defect and is now thepython -m scripts.file_converterform thatupdate_data.shalready used;tests/test_script_invocations.pyfails if a by-path invocation returns. Note thatparse_data()writes its report to the checkout’s committeddocs/data_report.rstwhatever-outit is given, somake testparseleaves that file describing the three-zone fixture - the target now says so.scripts/is type-checked by the mypy pre-commit hook instead of being excluded from it. The directory holds the data converter and the benchmark tooling - most of the repository’s non-library Python - and with nothing running mypy over it the annotations had drifted to fifteen errors: two# type: ignorecodes mypy no longer emits, so the ignore silenced nothing; two implicitOptionaldefaults thatno_implicit_optional = truewas already configured to reject; a dict annotated with a narrower value type than it is assigned; a bucket key and four bounding-box lists annotatedintwhileBoundariesdeclaresfloat; and two missing variable annotations. All fixed as annotations, with no runtime change. Two of the four errors mypy reported intests/auxiliaries.py, which it reaches by following imports out ofscripts/, are fixed alongside.test_scripts_are_type_checked_by_the_hookguards the exclude, which is a quieter way to stop type-checking a directory than theignore_errorslist the neighbouring tests already cover: it takes no override entry and reports nothing.The eight
__del__cleanup tests that differed only in which exceptioncleanup()raised, and whether zero or oneResourceWarningwas expected, are two parametrized tests over the suppressed and warned exception tuples. Each previously repeated the same subclass, the samecatch_warningsblock and the same filter, so adding a ninth exception to__del__’s suppression list meant copying the block a ninth time and a copy asserting the wrong count would be invisible. Coverage rises rather than falls: the hand-rolled loop asserting that__del__never raises to user code now runs over all six exception types instead of four.Three leftovers in the converter that read as bugs are gone:
has_coherent_sequencesbuilt an iterator solely to take its first element and then looped from the start anyway (correct, but it reads as an off-by-one),compile_bboxesunpacked a pair and immediately reassigned half of it, andprocess_single_hexreturned thehex_idit was handed so its only caller reassigned the loop variable to itself. Two shadowed builtins (diras a loop variable,idas a parameter) are renamed and three bare generator signatures annotated. The benchmark renderer classifies its “other” group by name suffix, as the two lines above it do, rather than by deep-equality scan over lists of dicts; and thecheck-manifestignore list drops two entries naming files that do not exist (CONTRIBUTING.rst,publish.py). Every converter change was verified by parsingtests/test_input.jsonbefore and after and comparing the outputs byte for byte.
8.2.5 (2026-07-11)
updated the data to 2026c
8.2.4 (2026-05-01)
added
manylinux_2_28_x86_64wheel to releases, fixing the fallback to version 6.0.1 when pip resolves with--platform manylinux_2_28_x86_64(Python 3.14 + numpy 2.4). . Thanks to theirix for the PR #420
8.2.3 (2026-04-30)
updated the data to 2026b
added examples and documentation for using parallel processing with the timezonefinder libary.
Internal:
reduced code duplication in coordinate validators: extracted common validation logic into a reusable
_validate_coordinate()helper functionimproved error handling and warning emission during
__del__resource cleanup- refactored
command_line.pyfor improved maintainability: decomposed monolithic
main()function into focused, independently testable components:_parse_arguments(),_lookup_timezone(), and_print_lookup_details()reduced cyclomatic complexity and improved separation of concerns
- refactored
- modernized codebase with Python 3.11+ features and best practices:
migrated from
typingmodule imports tocollections.abcforIterableandCallableadded
Selftype annotation for context manager protocolreplaced conditional dispatches with
match/casestatements for improved clarity and maintainability
using python 3.10+ type hints. Thanks to Marco Barbosa
- enhanced test coverage:
added 8 comprehensive thread safety tests for concurrent singleton initialization
added 37 coordinate validation tests covering edge cases (NaN, Inf, boundary values)
- comprehensive code quality improvements for production-grade stability:
improved exception handling: replaced bare
exceptclauses with specific exception types (FileNotFoundError,OSError,IOError), added proper exception chaining viafrom eenhanced type hints: added complete type annotations to public APIs, resolved type checking issues with mypy
enriched documentation: added comprehensive module and function docstrings with parameter descriptions, return types, error documentation, and usage examples
explicit API exports: added
__all__declarations toutils.py,zone_names.py,global_functions.py, andconfigs.pyfor clearer public API surfaceimproved error messages: replaced vague errors with specific context including valid ranges, expected values, and data locations
fixed deprecated patterns: updated
tempfileAPI usage to modern context managers, fixed import ordering (stdlib first)enhanced validation: added type checking for string inputs, better coordinate validation with clear error messages
8.2.2 (2026-03-26)
updated the data to 2026a
8.2.1 (2026-01-10)
updated the data to 2025c
require
numpy >=2following the official NumPy Deprecation Policydropped official support for Python 3.9 and 3.10, due to NumPy dropping support for these versions.
Internal:
Introduced
slowtest marker for computationally expensive tests to improve CI performance and local development workflow. Updated CI configuration, tox environments, Makefile targets, and documentation accordingly. Thanks to Chase Horton for the PR.enabled Numba tests for Python 3.13 and 3.14.
8.2.0 (2025-12-23)
Reverted to the full timezone dataset: Starting from this release,
timezonefinderuses the full originaltimezones-with-oceansdataset instead of the reducedtimezones-nowdataset. This restores access to all >440 original timezone names, providing full localization capabilities and historical timezone accuracy. The reduced dataset (with ~90 timezones) is still available via theparse_data.shscript for users who prefer the smaller memory footprint. Due to the discussion in GitHub Issue #363Improved error handling for resource cleanup. Thanks to Dave Tapley for the PR #375.
Bug fixed where on termination we may hit an exception attempting to close resources in
FileCoordAccessor. Thanks to David Park for the PR #377.Made CFFI extension builds fallible, allowing the build process to continue even if C extension compilation fails. Thanks to theirix for the PR #369.
Added Python 3.14 to the supported test matrix (tox + GitHub Actions).
8.1.0 (2025-09-22)
add the support of using the TimeZonefinder class instances as context managers. added a basic usage examples
note that the performance of certain_timezone_at() degraded drastically, since now many more polygons will be checked with an expensive point-in-polygon algorithm. consider using timezone_at() or timezone_at_land() instead.
introduced hybrid shortcut index structure that combines the functionality of separate shortcuts and unique shortcuts into a single optimized data structure, improving performance and reducing memory usage
zone id storage now defaults to
uint8and can be overridden via--zone-id-dtype/TIMEZONEFINDER_ZONE_ID_DTYPEwhen recompiling binariesrelax
cffiupper bound to allow the 2.x series so downstream packages pinningcffi>=2.0resolve cleanlyscripts/reporting.pycan now be executed as a standalone script to generate data reports from binary files independent fromfile_converter.pythe
check_speed_*.pyscripts now generates a detailed performance reports in reStructuredText format automatically included into documentation
Internal:
using abi3 (aka Python limited API) wheels to avoid a combinatory explosion with Python version. It allows the use of a single Python 3.9 base and building future-proof wheels. Thanks to theirix
using pydantic to validating and parsing the GeoJSON dataset. Thanks to ARYAN RAJ for the PR.
refactored file_converter.py to improve code quality. Thanks to Pratyush Kumar for the PR.
consolidated shortcut data structures: replaced
shortcuts.fbsfile withhybrid_shortcuts_uint8.fbs(orhybrid_shortcuts_uint16.fbs) file that stores both polygon lists and direct zone IDs using the minimal dtype for zone IDs.
8.0.0 (2025-08-11)
starting from this release,
timezonefinderuses the reducedtimezones-nowdataset version (cf. GitHub Discussion )in this dataset version, all timezones which agree on timekeeping methods as of the release date of the dataset, are merged into one zone (cf. Dataset Documentation ). This results in a reduced set of ~90 timezones instead of >440 timezones and a reduced memory footprint of the package.
If you used
timezonefinderfor localisation beyond the timezone behavior, it might become necessary for you to individually parse the full original dataset version using theparse_data.shscript.extended the
parse_data.shscript to support downloading thetimezones-nowDatasetadapted tests to the reduced dataset version
7.0.2 (2025-08-06)
exclude tests package closing issue #330
7.0.1 (2025-07-24)
hit PyPI project size limit. triggering re-upload to fix missing sdist in
7.0.0release.deleted all PyPI releases up to version
3.4.2(last version supporting python 2.7) to free up project space
7.0.0 (2025-07-21)
Simplified API for end-users, reducing redundant code
- Added global functions that use a shared
TimezoneFinderinstance: timezone_attimezone_at_landunique_timezone_atcertain_timezone_atget_geometry
- Added global functions that use a shared
Documented usage and warned about thread safety considerations for global functions
Updated command line interface to use global functions where appropriate
breaking API Changes: clarified naming. renamed “boundary” to “bbox”. renamed “polygon” to “boundary”. boundaries (the outer polygon defining part of a timezone) and holes are both polygons so hence the name “polygon” is ambiguous.
6.6.3 (2025-07-21)
when
in_memory=True, all polygonnumpyarrays are constructed once during startup rather than repeatedly on demand. This should significantly improve performance for applications that make frequent polygons queries.Created a
coord_accessors.pymodule for abstracting access to polygon coordinates, allowing for both in-memory and file-based access.added auto-generated data report to the documentation. thanks to ARYAN RAJ for the PR.
6.6.2 (2025-07-19)
hotfix missing hole_registry.json in the distributions
added integration tests in CI/CD. Thanks to theirix
6.6.1 (2025-07-18)
hotfix missing flatbuf module in the distributions
added tests for checking the content of the distributions
6.6.0 (2025-07-17)
major internal refactoring without breaking API changes. improvements for performance and code quality
use flatbuffer binary files for storing the polygon coordinate data and the shorcuts (spatial h3 index) in binary format. removed any custom code for reading and writing binary files (e.g. seek operations)
documented the binary format in the documentation
grouping all data files into a single “data” folder
added a new class PolygonArray to abstract away handling binary data of multiple polygons
separate binary data storage folders for polygon boundaries and holes. handling both with the PolygonArray class
parameterised location tests
improved CLI code quality suppressing any output. added nicer output in verbose mode
dropped support for python 3.8 (reached the end of life). thanks to ARYAN RAJ for the PR.
added support for official for python 3.12
added usage example scripts
switched from poetry to uv for dependency management and packaging. Thanks to theirix
6.5.9 (2025-03-25)
updated the timezone boundary data to version 2025b. Thanks to WestonReed
6.5.8 (2025-01-21)
updated the data to 2025a
internal: updated
file_converter.pyforh3>=4
6.5.7 (2024-12-02)
improved error handling to catch
ValueError: not enough values to unpack(Issue #209)
6.5.6 (2024-12-02)
add musllinux Wheels for Linux. Thanks to Pxli9130
6.5.5 (2024-11-20)
using
setuptoolsonly as a build dependency. Thanks to Kristian Sloth Lauszus
6.5.4 (2024-10-22)
using the dependency
h3>4. Thanks to Greg Meyer
6.5.3 (2024-09-16)
updated the data to 2024b.
refactored C lang point in polygon utils
6.5.2 (2024-06-17)
added support for
numpy>=2.0(fixes issue #234)
6.5.1 (2024-06-14)
added support for
cibuildwheel: publish wheels including the native C extension. GHA CI/CD pipeline creates sdist (no binaries inside) and a bunch of binary wheels with a prebuilt clang-pip extension for each python version. Thanks to theirix
6.5.0 (2024-03-14)
updated the data to 2024a.
internal:
use ruff linter in pre-commit hook
make dependency specifications less strict
6.4.1 (2024-02-08)
added official support for python 3.8 again, by specifying numba as multiple constraint dependency
internal:
added unit tests for polygon boundary binary reading
6.4.0 (2024-02-02)
added python 3.12 support (supported by numba since release 0.59.0), Closes #208
dropped official support for python 3.8, because the optional dependency numba requires python 3.9. this package might still work with python 3.8, but it is not tested anymore.
6.3.0 (2024-02-01)
updated the data to 2023d.
internal:
added docstrings. Thanks to Tyler Huntley
automatically skip GitHub actions publishing when the version already exists. useful for minor improvements without publishing a version. build would always fail otherwise
enable tests for python 3.11 with numba
enable tests for python 3.12
added tests for generating the documentation
use poetry dependency group specification (closing #199)
6.2.0 (2023-03-26)
updated the data to 2023b.
6.1.10 (2023-03-22)
added a pytz extra for easily maintaining compatibility
improved documentation
6.1.9 (2022-12-06)
updated the data to 2022g.
6.1.8 (2022-11-25)
pumped
h3dependency to>=3.7.6,<4to support python 3.11 (FIX #170)added python 3.11 tests (not yet supporting numba)
6.1.7 (2022-11-20)
updated the data to 2022f.
pinning dependencies more strictly
6.1.6 (2022-10-30)
updated the data to 2022d.
6.1.5 (2022-10-25)
updated the data to 2022b.
logging build failures with warnings
6.1.4 (2022-10-23)
more permissive optional
Numbadependency specification (FIX #162, impossible using latest numpy version)made all dependency specifications more permissive following the same rationale
6.1.3 (2022-09-23)
bugfix broken package build in the case of a broken
cffiinstallation (GitHub issue #155). Skip build process ifcffifails. For performance reasons using the C extension should remain the default behavior. Hence thecffidependency should not be optional.
6.1.2 (2022-09-13)
bugfix potentially broken pip install due to a mismatch in
cffiversions (GitHub issue #151)
6.1.1 (2022-08-18)
internals:
- minimized and cleaned up installation footprint (addresses GitHub Issue #151):
excluded script, changelog etc. files
included C extension into the “timezonefinder” package folder
added initialisation speed benchmark
6.1.0 (2022-08-15)
included point-in-polygon implementation in C
included build script to (optionally) build C point-in-polygon extension automatically during installation
added
cffias a dependency to build and interact with the C extensionimproved initialisation speed: read timezone polygon id index (h3 mapping) with
np.fromfileimproved CLI speed: construct TimezoneFinder() instances only on demand
internals:
updated documentation:
Numbainstallation is no longer recommended (it is a huge dependency and should be optional)clarified documentation: TimezoneFinder() instances should be reused
added separate speed benchmark scripts for point in polygon algorithm implementations and the different timezone finding functions
added separate section in the documentation for performance including speed benchmark results
added checks if all timezone polygons are actually in use (appear in index) to the file conversion script
added and improved utility functions as well as tests
improved typing
6.0.2 (2022-07-08)
bump numpy dependency version to
1.22(vulnerability fix)officially supported python versions
>=3.8,<3.11(due to numpy and numba constraints)packaging now completely based on pyproject.toml (poetry)
6.0.1 (2022-05-20)
explicitly included
py.typedin the package to allow mypy users to run static type checking
6.0.0 (2022-05-09)
breaking changes:
new dependency: using h3 for indexing the timezone polygons to check (“shortcuts) instead of the previous own indexing implementation. technical details: storing all 41,162 hex cells at resolution 3 and the corresponding timezone polygons which appear in them in the
shortcuts.bin(~500 KB).removed
.closest_timezone_at(): with the current data set with ocean zones in use, any point is included in some zone. it is therefore not meaningful to search for the closest boundary! Also the timezone polygons do NOT follow the shorelines. This makes the results ofclosest_timezone_at()somewhat less expressive. Maintaining the non-trivial distance computation algorithms is not really at the core responsibility of this package.officially only supporting
python>=3.7(removed official support forpython3.6, since thenumpydependency did so)removed
vfrom the github release/version tags
internals:
updated the data to 2021c. please note that timezone polygons might be overlapping (cf. e.g. timezone-boundary-builder/issue/105) and that hence a query coordinate can actually match multiple time zones.
timezonefinderdoes currently NOT support such multiplicity and will always only return the first found match.shortcuts: sorting according to size of polygons (amount of coordinates) instead of the count of zone ids. useful as optimisation: smaller polygons will be checked first and can hence be “ruled-out” faster
“most common”: now meaning the zone with the largest polygons in the shortcut (last in the shortcut sorting). please note that this does not necessarily mean the most area in the shortcut is covered by this zone. the polygon size is just an easier to compute heuristic.
officially supporting python versions >=3.7,<3.11 (like
numba)using poetry for dependency management
using GitHub actions for CI instead of travis
some minor typing improvements
pre-commit hook improvements
In case you have criticism or feedback please reach out by creating an issue, discussion or PR on GitHub.
5.2.0 (2021-02-09)
added function
unique_timezone_at()(based on the request in issue #112). Allows querying for the unique zone within the corresponding shortcut.
5.1.1 (2021-02-03)
BUGFIX: get_geometry() now also works for the last zone
add get_geometry() tests
black code style
pre-commit checks
5.1.0 (2021-01-14)
update the command line interface. the package can now directly be called with
timezonefinderadded the new query functions to the command line interface (to match the online API)
5.0.0 (2020-12-23)
MAJOR CHANGES:
Due to multiple user requests the ocean timezones (“Etc/GMT+-XX”) are now included in the data files per default. fix #88. Since ocean timezones span the whole globe, now every point lies within a timezone!
API changes:
* added timezone_at_land(): replaces the previous timezone_at() and returns None in case of a matched ocean timezone.
deprecated
certain_timezone_at(). only meaningful in the case of timezone data WITHOUT oceans. Has equal results astimezone_at(), but is more expensive to use.also looking a single closest timezone boundary with
closest_timezone_at()is not really meaningful, since every point lies within a zone!refactored tests. new test cases for ocean timezones
4.5.0 (2020-11-06)
BUGFIX: handle output destination for data files correctly in file_converter.py (FIX #107)
updated the data to 2020d
disable a test case for an Uzbek enclave. tests fail at this coordinate, possibly a bug. issue filed here: https://github.com/evansiroky/timezone-boundary-builder/issues/94
update parse_data.sh script to properly handle new data format
4.4.1 (2020-08-04)
BUGFIX: a longitude of 180 equals -180 (not 0.0 as previously implemented)
4.4.0 (2020-05-14)
added new class TimezonefinderL for using JUST shortcuts (without timezone polygon data)
therefore included the most common timezone of each shortcut stored in the binary file
shortcuts_direct_id.binintroduced typing
included API documentation
read hole registry directly from json,
hole_poly_ids.binnot required any moreadded the
parse_data.shshell script for downloading the latest timezone data, also with oceans
improvements of file_converter.py:
added command line arguments for specifying the input and output directories
read binary names from
global_settings.pyread data types from
global_settings.pyuse with statement for writing binaries
automatically detect overflow for each data type in use
cleanup code, remove redundancies, improve codestyle
fixing #101: make imports work for local and remote execution
4.3.1 (2020-04-29)
BUGFIX #99: include the correct timezone_names.json in build
wheel specific for the supported python versions (3.6, 3.7, 3.8)
4.3.0 (2020-04-28)
updated the data to 2020a
added “extra” simplifying the installation of Numba
added minimal required python version
added minimal required version of the dependencies
simplified and updated settings (e.g. reading current version from file)
also testing python 3.8 now
loading version from file
4.2.0 (2019-12-15)
added option to specify the location of the binary data files to use. making it possible to easily point to own compiled data. also load timezone names json from this location
make timezone names a class attribute (instead of a global variable)
simplify code for opening and closing multiple binary files
added tests for a specified path to the data
testing multiple python3 versions automatically
pinned new requirements
importlib_resources removed from the dependencies
added a documentation at: https://timezonefinder.readthedocs.io/en/latest/
added contribution guidelines
4.1.0 (2019-07-07)
updated the data to 2019b
added description of using vectorized input in readme
4.0.3 (2019-06-23)
clarification of readme: referenced latest timezonefinderL release, better rst headlines, updated shield.io banner syntax
clarification of speedup times (exponential notation)
removed six and py2 dependency from tests
minor updates to publishing routine
minor improvement in timezone_at(): conversion coordinates to int later only when required
4.0.2 (2019-04-01)
updated the data to 2019a
4.0.1 (2019-03-12)
BUGFIX: fixing #77 (missing dependency in setup.py)
4.0.0 (2019-03-12)
ATTENTION: Dropped Python2 support (#72)! six dependency no longer required.
BUGFIX: fixing #74 (broken py3 with numba support)
added in_memory-mode (adapted unit tests to test both modes, added speed tests and explanation to readme)
use of timeit in speed tests for more accurate results
dropped use of kwargs_only decorator (can be implemented directly with python3)
3.4.2 (2019-01-15)
BUGFIX: fixing #70 (broken py2.7 with numba support)
added automatic tox tests for py2.7 py3 environments with numba installed
fixed coverage report
3.4.1 (2019-01-13)
added test cases for the Numba helpers (#55)
added more polygon tests to test the function inside_polygon()
added global data type definitions (format strings) to
global_settings.pyremoved tzwhere completely from the main tests (no comparison any more).
removed code drafts for ahead of time compilation (#40)
3.4.0 (2019-01-06)
updated the data to 2018i
introduced
global_settings.pyto globally define settings and get rid of “magic numbers”.
3.3.0 (2018-11-17)
updated the data to 2018g
3.2.1 (2018-10-30)
ATTENTION: the package
importlib_resourcesis now requiredfixing automatic Conda build by exchanging
pkg_resources.resource_streamwithimportlib_resources.open_binaryadded tests for overflow in helpers.py/inside_polygon()
3.2.0 (2018-10-23)
ATTENTION: the package kwargs_only is not a requirement any more!
fixing #63 (kwargs_only not in conda) enabling automatic conda forge builds by directly providing the kwargs_only functionality again
added example.py with the code examples from the readme
fixing #62 (overflow happening because of using numpy.int32): forcing int64 type conversion
3.1.0 (2018-09-27)
fixing typo in requirements.txt
updated publishing routine: reminder to include all direct dependencies and to compile the requirements.txt with python 2 (pip-tools)
3.0.2 (2018-09-26)
ATTENTION: the package kwargs_only is now required! This functionality has previously been implemented by the author directly within this package, but some code features got deprecated.
updated build/testing/publishing routine
fixing issue #61 (six dependency not listed in setup.py)
no more default arguments for timezone_at() and certain_timezone_at()
no more comparison to (py-)tzwhere in the tests (test_it.py)
updated requirements.txt (removed tzwhere and dependencies)
prepared helpers_test.py for also testing helpers_numba.py
exchanged deprecated inspect.getargspec() into .getfullargspec() in functional.py
3.0.1 (2018-05-30)
fixing minor issue #58 (readme not rendering in pyPI)
3.0.0 (2018-05-17)
ATTENTION: the package six is now required! (was necessary because of the new testing routine. improves compatibility standards)
updated build/testing/publishing routine
updated the data to 2018d
fixing minor issue #52 (shortcuts being out of bounds for extreme coordinate values)
- the list of polygon ids in each shortcut is sorted after freq. of appearance of their zone id.
this is critical for ruling out zones faster (as soon as just polygons of one zone are left this zone can be returned)
using argparse package now for parsing the command line arguments
added option of choosing between functions timezone_at() and certain_timezone_at() on the command line with flag -f
the timezone names are now being stored in a readable JSON file
adjusted the main test cases
corrections and clarifications in the readme and code comments
2.1.2 (2017-11-20)
bugfix: possibly uninitialized variable in closest_timezone_at()
2.1.1 (2017-11-20)
updated the data to 2017c
minor improvements in code style and readme
include publishing routine script
2.1.0 (2017-05-19)
updated the data to 2017a (tz_world is not being maintained any more)
the file_converter has been updated to parse the new format of .json files
the new data is much bigger (based on OSM Data, +40MB). I am sorry for this but its still better than small outdated data!
in case size and speed matter more you than actuality, you can still check out older versions of timezonefinder(L)
the new timezone polygons are not limited to the coastlines, but they are including some large parts of the sea. This makes the results of closest_timezone_at() somewhat meaningless (as with timezonefinderL).
the polygons can not be simplified much more and as a consequence timezonefinderL is not being updated any more.
simplification functions (used for compiling the data for timezonefinderL) have been deleted from the file_converter
the readme has been updated to inform about this major change
some tests have been temporarily disabled (with tzwhere still using a very old version of tz_world, a comparison does not make too much sense atm)
2.0.1 (2017-04-08)
added missing package data entries (2.0.0 didn’t include all necessary .bin files)
2.0.0 (2017-04-07)
- ATTENTION: major change!: there is a second version of timezonefinder now: timezonefinderL. There the data has been simplified
for increasing speed reducing data size. Around 56% of the coordinates of the timezone polygons have been deleted there. Around 60% of the polygons (mostly small islands) have been included in the simplified polygons. For any coordinate on landmass the results should stay the same, but accuracy at the shorelines is lost. This eradicates the usefulness of closest_timezone_at() and certain_timezone_at() but the main use case for this package (= determining the timezone of a point on landmass) is improved. In this repo timezonefinder will still be maintained with the detailed (unsimplified) data.
file_converter.py has been complemented and modified to perform those simplifications
introduction of new function get_geometry() for querying timezones for their geometric shape
added shortcuts_unique_id.bin for instantly returning an id if the shortcut corresponding to the coords only contains polygons of one zone
data is now stored in separate binaries for ease of debugging and readability
polygons are stored sorted after their timezone id and size
timezonefinder can now be called directly as a script (experimental with reduced functionality, cf. readme)
optimisations on point in polygon algorithm
small simplifications in the helper functions
clarification of the readme
clarification of the comments in the code
referenced the new conda-feedstock in the readme
referenced the new timezonefinder API/GUI
1.5.7 (2016-07-21)
ATTENTION: API BREAK: all functions are now keyword-args only (to prevent lng lat mix-up errors)
fixed a little bug with too many arguments in a @jit function
clarified usage of the package in the readme
prepared the usage of the ahead of time compilation functionality of Numba. It is not enabled yet.
sorting the order of polygons to check in the order of how often their zones appear, gives a speed bonus (for closest_timezone_at)
1.5.6 (2016-06-16)
using little endian encoding now
introduced test for checking the proper functionality of the helper functions
wrote tests for proximity algorithms
improved proximity algorithms: introduced exact_computation, return_distances and force_evaluation functionality (s. Readme or documentation for more info)
1.5.5 (2016-06-03)
using the newest version (2016d, May 2016) of the tz world data
holes in the polygons which are stored in the tz_world data are now correctly stored and handled
rewrote the file_converter for storing the holes at the end of the timezone_data.bin
added specific test cases for hole handling
made some optimizations in the algorithms
1.5.4 (2016-04-26)
using the newest version (2016b) of the tz world data
rewrote the file_converter for parsing a .json created from the tz_worlds .shp
had to temporarily fix one polygon manually which had the invalid TZID: ‘America/Monterey’ (should be ‘America/Monterrey’)
had to make tests less strict because tzwhere still used the old data at the time and some results were simply different now
1.5.3 (2016-04-23)
using 32-bit ints for storing the polygons now (instead of 64-bit): I calculated that the minimum accuracy (at the equator) is 1cm with the encoding being used. Tests passed.
Benefits: 18MB file instead of 35MB, another 10-30% speed boost (depending on your hardware)
1.5.2 (2016-04-20)
added python 2.7.6 support: replaced strings in unpack (unsupported by python 2.7.6 or earlier) with byte strings
timezone names are now loaded from a separate file for better modularity
1.5.1 (2016-04-18)
- added python 2.7.8+ support:
Therefore I had to change the tests a little bit (some operations were not supported). This only affects output. I also had to replace one part of the algorithms to prevent overflow in Python 2.7
1.5.0 (2016-04-12)
automatically using optimized algorithms now (when numba is installed)
added TimezoneFinder.using_numba() function to check if the import worked
1.4.0 (2016-04-07)
- Added the
file_converter.pyto the repository: It converts the .csv from pytzwhere to another.csvand this one into the used.bin. Especially the shortcut computation and the boundary storage in there save a lot of reading and computation time, when deciding which timezone the coordinates are in. It will help to keep the package up to date, even when the timezone data should change in the future.
- Added the