Skip to content

Bucket offline way assignment by area group - #113

Closed
FrogAi wants to merge 3 commits into
pfeiferj:mainfrom
FrogAi:codex/bucket-offline-way-assignment
Closed

Bucket offline way assignment by area group#113
FrogAi wants to merge 3 commits into
pfeiferj:mainfrom
FrogAi:codex/bucket-offline-way-assignment

Conversation

@FrogAi

@FrogAi FrogAi commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Bucket scanned ways by the selected 2-degree area groups before assigning them to 0.25-degree cells.
  • Keep ways spanning multiple groups once in a shared scanner-ordered list, merge both candidate lists by scan index, and reapply the existing exact cell-overlap predicate.
  • Use the original full scan for negative or non-finite overlap values and invalid way bounds.

Motivation

Offline generation currently tests every scanned way against every selected cell. A 20-degree by 20-degree generation chunk contains 6,400 quarter-degree cells, so a 73,135-way input performs 468,064,000 cell-level overlap checks before serialization.

The generator already organizes output into 2-degree directory groups. This change uses those same groups as a conservative candidate index. Ways belonging to one group are checked only against that group's cells. Ways that can reach multiple groups are retained once in a shared list and checked where needed. The final membership decision remains the existing way.Box.Overlapping(area.OverlapBox(s.Overlap)) predicate.

Performance

The benchmark used the same normalized Delaware PBF, 20-degree by 20-degree output box, 0.001-degree overlap, filesystem, and pinned Linux/amd64 Go 1.25.1 environment for both revisions. Each result is the median of ten measured process runs after one warm-up per revision, using repeated ABBA ordering.

Standalone F-10c Base (68813e0) Head (01405ab) Difference
Generation time 3,964.102 ms 667.325 ms -83.17% (5.94x)
Time IQR 19.121 ms 3.729 ms -
Total Go allocation 460.626 MiB 463.716 MiB +3.090 MiB
Median peak RSS 373.619 MiB 378.574 MiB +4.955 MiB
Output 6,400 files / 21,302,021 bytes 6,400 files / 21,302,021 bytes byte-identical

The optimization was also applied on top of PR #97's exact area grid and PR #112's empty-cell suppression, then measured with GenerateEmptyFiles=false:

Composed with #97 and #112 Composed base Composed head Difference
Generation time 3,631.780 ms 410.274 ms -88.70% (8.85x)
Time IQR 20.363 ms 10.504 ms -
Total Go allocation 445.932 MiB 449.017 MiB +3.085 MiB
Median peak RSS 265.113 MiB 284.557 MiB +19.444 MiB
Output 44 files / 21,069,552 bytes 44 files / 21,069,552 bytes byte-identical

Peak RSS is reported directly rather than inferred from Go allocation and varied more than elapsed time across runs. The benchmark is a chunk-shaped sparse Delaware workload, not a planet-wide density estimate.

Candidate reduction

The same fixture produced this assignment count:

Metric Count
Scanned ways 73,135
Selected cells 6,400
Selected 2-degree groups 100
Base cell-level checks 468,064,000
Group-index checks (upper bound) 7,313,500
Head cell-level candidate checks 4,699,648
Retained way indexes 73,135
Multi-group shared ways 3

The group-index row is an upper bound because the per-way group scan stops at the second match, so the three multi-group ways terminate early. Including index construction, overlap checks fell from 468,064,000 to at most 12,013,148 for this fixture. Each scanned way contributes at most one retained index, so widespread ways do not create a way-by-group memory cross-product.

Validation

An external comparison harness exercised the final source against base 68813e05b4db68764b7d9dbb73b7998726223c45:

  • Compared exact cell assignments for zero, positive, large, negative, infinite, and NaN overlap values.
  • Covered group and cell edges, corner touches, negative coordinates, world edges, long AABBs, duplicate OSM IDs, planar dateline behavior, inverted bounds, and non-finite bounds.
  • Confirmed unsafe inputs select the original streaming scan and retain no bucket data.
  • Confirmed single-group and multi-group candidate lists merge back into original scanner order without duplication.
  • Confirmed each scanned way retains at most one candidate index.
  • Produced the same 6,400 relative paths, total bytes, and output digest in the standalone benchmark: 0ab87ac90380b4ed2d28cc898f64b4c0b069a68adb0717bb5280ce46b7ccb195.
  • Produced the same 44 relative paths, total bytes, and output digest when composed with Avoid allocating global offline area grid #97 and Skip generated cells with no assigned ways #112: bdfbc23fab9e4afb8475ae73b77e445ef97a5118091da37e5486450a35121022.
  • go test ./..., go vet ./..., and go build ./... passed on Linux/amd64 with Go 1.25.1.
  • FrogAi Actions Build #51 passed for exact commit e45faaa1b2fe4cd82160340670962aa44e9ae4bd. That workflow only compiles; it does not run go test, go vet, or gofmt.
  • The benchmarks and the comparison harness above were measured on 01405abb5fe7059d0f8934d31df903c49f84935e. The current head adds two behavior-preserving cleanups on top: the standard library math import is no longer aliased, matching maps/way.go and maps/offline.go, and the merged way index is declared with var instead of a discarded zero initializer.

The fixture contains 73,135 located ways and has SHA-256 7695b17693df18f2f76a115f4d7f2385034f7b89b8f1d82ba9e2ea088993addc.

Compatibility

  • No offline schema, file format, filename, CLI, or settings changes.
  • Selected-cell order, way scan order, duplicate records, inclusive overlap behavior, and serialization remain unchanged.
  • Negative or non-finite overlap and invalid way bounds retain the original full-scan behavior.
  • Ways spanning multiple groups can still approach the original per-cell work; the optimization does not claim a universally linear bound.
  • This PR does not include PR Avoid allocating global offline area grid #97's area-grid correction or PR Skip generated cells with no assigned ways #112's empty-cell suppression. The composed benchmark verifies that the optimization remains effective with both changes.
  • This affects offline map generation only; runtime lookup and driving behavior are unchanged.

Merge order with #112 — read before resolving

Merging this PR with #112 conflicts, and the resolution a merge tool suggests silently doubles every way in every tile. #112 moves the way-assignment loop; this PR rewrites it in place. git merge-tree confirms git auto-applies the plain for _, way := range scannedWays loop outside the conflict region, marking only the deletion as conflicting — so keeping the HEAD side, the obvious choice, leaves both loops.

The result compiles, vets, passes go test, and goes green in CI while writing every way twice. Reproduced concretely: cell 10.000000_20.000000_10.250000_20.250000 receives [100 100] instead of [100]; a full run measured 151,994 ways with 75,997 duplicate IDs and 2x tile size. The opposite resolution fails loudly with unused locals, so keep-both is the only silently-wrong option, and it is the one a merge tool picks.

This is symmetric — not a defect in either PR — and would arise with any pair editing that loop. Whichever lands second must delete the auto-applied plain loop and move this PR's if bucketed {...} else {...} block up into its place. #97 now merges with zero manual resolution, since the stdmath import alias that caused its conflict was removed in this PR's second commit.

Known limits, deliberately not changed here

  • The index build is O(ways x groups) on the default path. maps/generate_offline.go ranges every group per way and breaks only on a second match, so 0- and 1-match ways scan all 16,200 groups at whole-world bounds — measured ~163-214 us/way, roughly 18 minutes for a 5M-way extract, with no log line before it (it runs ahead of Finding Bounds, so it presents as a hang). This is still a large net win over base, which extrapolates to ~11.7 hours, so it is a missed optimization rather than a regression. Box.GroupPos() could answer it in O(1), but that rewrite has a real edge case around math.Floor versus the inclusive edges of Overlapping, so it is left for a follow-up.
  • canBucketWay is kept even though it is provably dead today. Independent proofs established that no TmpWay the osmpbf decoder can produce fails it: coordinates are int64-derived so cannot be NaN or Inf, and the min/max fold cannot invert when len(Nodes) > 1. It is retained anyway because the proof depends on a third-party decoder's internals and the failure mode if that ever changes is silently dropped roads. It runs once per way during offline generation, not on the device. The separate s.Overlap guard is live via --overlap and verified load-bearing at overlap <= -0.126.
  • Bucketing narrows the AREA_BOX_DEGREES contract. settings/const.go permits "1.0 divided by an integer", but the group nesting is only exact for powers of two; 1/3 or 1/5 would leave 396 of 1296 cells poking outside their group and drop ways. Latent today because the configured value is a power of two.
  • Selections of one or two cells are 2.2-3.1x slower than base (crossover at about four cells; 40-70x faster beyond that).
  • No test accompanies a soundness-critical rewrite, because .github/workflows/build.yml runs no tests at all. If #99 lands, any of the audit harnesses used here trims into a real test in under 100 lines.

@FrogAi

FrogAi commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Closing this version. The measured offline-generation speedup is real, but the implementation adds a soundness-critical index with tradeoffs that are not acceptable for a performance-only change: an O(ways x groups) prepass that becomes multi-minute at whole-world scale, slower one- and two-cell selections, and a narrower latent AREA_BOX_DEGREES contract. A future optimization should use direct group lookup or another simpler index and re-establish byte-for-byte parity and end-to-end benefit against current main. The branch and benchmark evidence remain preserved.

@FrogAi FrogAi closed this Aug 10, 2026
@FrogAi
FrogAi deleted the codex/bucket-offline-way-assignment branch August 10, 2026 04:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant