feat(block2): geohash encoder in commons_core (for 'use my location')
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.
This commit is contained in:
parent
98110a6474
commit
cb5f55e146
3 changed files with 92 additions and 0 deletions
|
|
@ -12,6 +12,7 @@ export 'src/identity/identity_service.dart';
|
||||||
export 'src/identity/nostr_key_derivation.dart';
|
export 'src/identity/nostr_key_derivation.dart';
|
||||||
export 'src/ids/id_gen.dart';
|
export 'src/ids/id_gen.dart';
|
||||||
export 'src/social/certification.dart';
|
export 'src/social/certification.dart';
|
||||||
|
export 'src/social/geohash.dart';
|
||||||
export 'src/social/message_transport.dart';
|
export 'src/social/message_transport.dart';
|
||||||
export 'src/social/nostr/nostr_connection.dart';
|
export 'src/social/nostr/nostr_connection.dart';
|
||||||
export 'src/social/nostr/nostr_message_transport.dart';
|
export 'src/social/nostr/nostr_message_transport.dart';
|
||||||
|
|
|
||||||
56
packages/commons_core/lib/src/social/geohash.dart
Normal file
56
packages/commons_core/lib/src/social/geohash.dart
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
35
packages/commons_core/test/social/geohash_test.dart
Normal file
35
packages/commons_core/test/social/geohash_test.dart
Normal file
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue