From cb5f55e1461657363892bc630811e91d866feb9a Mon Sep 17 00:00:00 2001 From: vjrj Date: Fri, 10 Jul 2026 03:18:30 +0200 Subject: [PATCH] feat(block2): geohash encoder in commons_core (for 'use my location') MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure Dart lat/lon -> low-precision geohash (public-domain algorithm), the piece the coarse-area capture needs so people never type a code. Precision-controlled (5 chars ~ ±2.4 km). Tested against the canonical u4pruydqqvj vector + prefix/ proximity properties. 5 tests green. --- packages/commons_core/lib/commons_core.dart | 1 + .../commons_core/lib/src/social/geohash.dart | 56 +++++++++++++++++++ .../test/social/geohash_test.dart | 35 ++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 packages/commons_core/lib/src/social/geohash.dart create mode 100644 packages/commons_core/test/social/geohash_test.dart diff --git a/packages/commons_core/lib/commons_core.dart b/packages/commons_core/lib/commons_core.dart index 0b33cad..8fca064 100644 --- a/packages/commons_core/lib/commons_core.dart +++ b/packages/commons_core/lib/commons_core.dart @@ -12,6 +12,7 @@ export 'src/identity/identity_service.dart'; export 'src/identity/nostr_key_derivation.dart'; export 'src/ids/id_gen.dart'; export 'src/social/certification.dart'; +export 'src/social/geohash.dart'; export 'src/social/message_transport.dart'; export 'src/social/nostr/nostr_connection.dart'; export 'src/social/nostr/nostr_message_transport.dart'; diff --git a/packages/commons_core/lib/src/social/geohash.dart b/packages/commons_core/lib/src/social/geohash.dart new file mode 100644 index 0000000..fa1ba62 --- /dev/null +++ b/packages/commons_core/lib/src/social/geohash.dart @@ -0,0 +1,56 @@ +/// Geohash encoding (public-domain algorithm), pure Dart. Turns a latitude / +/// longitude into a short base-32 string whose length sets its precision — the +/// coarse "near you" area used by offers. Kept deliberately low-precision at the +/// call site so an exact position never leaves the device. +class Geohash { + const Geohash._(); + + static const _base32 = '0123456789bcdefghjkmnpqrstuvwxyz'; + + /// Encodes [latitude]/[longitude] to a geohash of [precision] characters. + /// Precision ≈ cell size: 5 ≈ ±2.4 km, 4 ≈ ±20 km, 3 ≈ ±78 km. + static String encode( + double latitude, + double longitude, { + int precision = 5, + }) { + if (precision < 1) throw ArgumentError.value(precision, 'precision'); + var latMin = -90.0, latMax = 90.0; + var lonMin = -180.0, lonMax = 180.0; + final out = StringBuffer(); + var bit = 0; + var ch = 0; + var even = true; // start with longitude + + while (out.length < precision) { + if (even) { + final mid = (lonMin + lonMax) / 2; + if (longitude >= mid) { + ch = (ch << 1) | 1; + lonMin = mid; + } else { + ch = ch << 1; + lonMax = mid; + } + } else { + final mid = (latMin + latMax) / 2; + if (latitude >= mid) { + ch = (ch << 1) | 1; + latMin = mid; + } else { + ch = ch << 1; + latMax = mid; + } + } + even = !even; + if (bit < 4) { + bit++; + } else { + out.write(_base32[ch]); + bit = 0; + ch = 0; + } + } + return out.toString(); + } +} diff --git a/packages/commons_core/test/social/geohash_test.dart b/packages/commons_core/test/social/geohash_test.dart new file mode 100644 index 0000000..10ada76 --- /dev/null +++ b/packages/commons_core/test/social/geohash_test.dart @@ -0,0 +1,35 @@ +import 'package:commons_core/commons_core.dart'; +import 'package:test/test.dart'; + +void main() { + group('Geohash.encode', () { + test('matches the classic reference vector', () { + // (57.64911, 10.40744) → "u4pruydqqvj" is the canonical geohash example. + expect(Geohash.encode(57.64911, 10.40744, precision: 11), + 'u4pruydqqvj'); + }); + + test('precision controls length and coarseness (prefix relation)', () { + final fine = Geohash.encode(57.64911, 10.40744, precision: 9); + final coarse = Geohash.encode(57.64911, 10.40744, precision: 5); + expect(coarse.length, 5); + expect(fine, startsWith(coarse)); // coarser is a prefix of finer + }); + + test('nearby points share a coarse prefix; far ones do not', () { + final a = Geohash.encode(41.39, 2.16, precision: 5); // Barcelona-ish + final b = Geohash.encode(41.40, 2.17, precision: 5); // ~1.5 km away + final far = Geohash.encode(40.41, -3.70, precision: 5); // Madrid + expect(a.substring(0, 3), b.substring(0, 3)); + expect(a, isNot(far)); + }); + + test('origin encodes without error', () { + expect(Geohash.encode(0, 0, precision: 5), hasLength(5)); + }); + + test('rejects non-positive precision', () { + expect(() => Geohash.encode(0, 0, precision: 0), throwsArgumentError); + }); + }); +}