diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5724873 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Alan Chu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index a3a24cc..a56688f 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,77 @@ # GeohashKit ![Swift](https://github.com/ualch9/GeohashKit/workflows/Swift/badge.svg) -GeohashKit is a native Swift implementation of the geohash hashing algorithem. Supporting encode, decode and neighbor search. This is an indirect fork of [maximveksler/Geohashkit](https://github.com/maximveksler/GeohashKit), meant for Swift Package Manager support. +GeohashKit is a native Swift implementation of the [Geohash hashing algorithm](https://www.movable-type.co.uk/scripts/geohash.html). Supporting encode, decode and neighbor search. + +The original Swift (v1) implementation is from [maximveksler/GeohashKit](https://github.com/maximveksler/GeohashKit). The v2 implementation uses a data structure to define the Geohash as a rectangular cell, rather than just a string. ## Platforms - iOS, macOS, watchOS, tvOS - Ubuntu -## API +## API (v2) +A `Geohash` is a data structure, representing a geohash as a rectangular cell. -### Encode +#### Initialize with coordinates ("Encode") ```swift -Geohash.encode(latitude: 42.6, longitude: -5.6) // "ezs42" +let geohash = Geohash(coordinates: (42.6, -5.6), precision: 5) +print(geohash.geohash) // "ezs42" ``` -###### Specify desired precision +#### Initialize with existing geohash ("Decode") ```swift -Geohash.encode(latitude: -25.382708, longitude: -49.265506, 12) // "6gkzwgjzn820" +let geohash = Geohash(geohash: "ezs42") +print(geohash.coordinates) // (latitude: 42.60498046875, longitude: -5.60302734375) ``` -### Decode +#### Neighbors ```swift -Geohash.decode("ezs42")! // (latitude: 42.60498046875, longitude: -5.60302734375) +let neighbors = Geohash(geohash: "u000").neighbors // Returns an array of neighbor geohash cells. +print(neighbors.north) // Get the north (top) cell. +print(geohash.neighbors.all.map { $0.geohash }) // ["u001", "u003", "u002", "spbr", "spbp", "ezzz", "gbpb", "gbpc"] ``` -### Neighbor Search +#### MapKit ```swift -Geohash.neighbors("u000")! // ["u001", "u003", "u002", "spbr", "spbp", "ezzz", "gbpb", "gbpc"] +let geohash = Geohash(geohash: "ezs42") +let region: MKCoordinateRegion = geohash.region ``` +#### Re: Precision +I purposely left out a precision enum. I found that explaining the approximate size of the cell at +a given precision was confusing, it is difficult to explain a size without using numbers (`case 2500km` is not valid Swift). +Geohashes are rectangular Mercator cells with true size dependent on the latitude, so +defining a `case twentyFiveHundredKilometers` is still not necessarily true. + +
+ v1 API (maximveksler/GeohashKit compatible) + To use the maximveksler-compatible API, checkout exactly `1.0`: + + ```swift + .package(url: "https://github.com/ualch9/geohashkit.git", .exact("1.0")) + ``` + + ### Encode + ```swift + Geohash.encode(latitude: 42.6, longitude: -5.6) // "ezs42" + ``` + + ### Specify desired precision + ```swift + Geohash.encode(latitude: -25.382708, longitude: -49.265506, 12) // "6gkzwgjzn820" + ``` + + ### Decode + ```swift + Geohash.decode("ezs42")! // (latitude: 42.60498046875, longitude: -5.60302734375) + ``` + + ### Neighbor Search + ```swift + Geohash.neighbors("u000")! // ["u001", "u003", "u002", "spbr", "spbp", "ezzz", "gbpb", "gbpc"] + ``` +
+ ## Install Use Swift Package Manager. diff --git a/Sources/GeohashKit/Geohash.swift b/Sources/GeohashKit/Geohash.swift index 0f66008..fd9b90e 100644 --- a/Sources/GeohashKit/Geohash.swift +++ b/Sources/GeohashKit/Geohash.swift @@ -3,13 +3,6 @@ // Original by Maxim Veksler. Redistributed under MIT license. // -enum CompassPoint { - case north // Top - case south // Bottom - case east // Right - case west // Left -} - enum Parity { case even, odd } @@ -18,58 +11,133 @@ prefix func !(a: Parity) -> Parity { return a == .even ? .odd : .even } +/// A geohash is a rectangular cell expressing a location using an ASCII string, the size of which is determined by how long the string is, the longer, the more precise. public struct Geohash { - public static let defaultPrecision = 5 + // MARK: - Types + enum CompassPoint { + /// Top + case north + + /// Bottom + case south + + /// Right + case east + + /// Left + case west + } + + public typealias Coordinates = (latitude: Double, longitude: Double) + public typealias Hash = String + // MARK: - Constants + public static let defaultPrecision = 5 private static let DecimalToBase32Map = Array("0123456789bcdefghjkmnpqrstuvwxyz") // decimal to 32base mapping (0 => "0", 31 => "z") private static let Base32BitflowInit: UInt8 = 0b10000 - // - MARK: Public - public static func encode(latitude: Double, longitude: Double, _ precision: Int = Geohash.defaultPrecision) -> String { - return geohashbox(latitude: latitude, longitude: longitude, precision)!.hash + // MARK: - Public properties + public var coordinates: Coordinates { + return (latitude, longitude) + } + + /// The latitude value (measured in degrees) of the center of the cell. + public var latitude: Double { + return (self.north + self.south) / 2 } - public static func decode(_ hash: String) -> (latitude: Double, longitude: Double)? { - return geohashbox(hash)?.point + /// The longitude value (measured in degrees) of the center of the cell. + public var longitude: Double { + return (self.east + self.west) / 2 + } + + /// The latitude/longitude delta (measured in degrees) of the cell, used for determining the dimensions of the cell. + public var size: Coordinates { + // * possible case examples: + // + // 1. bbox.north = 60, bbox.south = 40; point.latitude = 50, size.latitude = 20 ✅ + // 2. bbox.north = -40, bbox.south = -60; point.latitude = -50, size.latitude = 20 ✅ + // 3. bbox.north = 10, bbox.south = -10; point.latitude = 0, size.latitude = 20 ✅ + let latitude = north - south + + // * possible case examples: + // + // 1. bbox.east = 60, bbox.west = 40; point.longitude = 50, size.longitude = 20 ✅ + // 2. bbox.east = -40, bbox.west = -60; point.longitude = -50, size.longitude = 20 ✅ + // 3. bbox.east = 10, bbox.west = -10; point.longitude = 0, size.longitude = 20 ✅ + let longitude = east - west + + + return (latitude: latitude, longitude: longitude) } - public static func neighbors(_ centerHash: String) -> [String]? { - // neighbor precision *must* be them same as center'ed bounding box. - let precision = centerHash.count - - guard let box = geohashbox(centerHash), - let n = neighbor(box, direction: .north, precision: precision), // n - let s = neighbor(box, direction: .south, precision: precision), // s - let e = neighbor(box, direction: .east, precision: precision), // e - let w = neighbor(box, direction: .west, precision: precision), // w - let ne = neighbor(n, direction: .east, precision: precision), // ne - let nw = neighbor(n, direction: .west, precision: precision), // nw - let se = neighbor(s, direction: .east, precision: precision), // se - let sw = neighbor(s, direction: .west, precision: precision) // sw - else { return nil } - - // in clockwise order - return [n.hash, ne.hash, e.hash, se.hash, s.hash, sw.hash, w.hash, nw.hash] + public let geohash: Hash + + /// The number of characters in the hash. + /// Refer to the table below for approximate cell size. + /// ``` + /// Precision Cell width Cell height + /// 1 ≤ 5,000km x 5,000km + /// 2 ≤ 1,250km x 625km + /// 3 ≤ 156km x 156km + /// 4 ≤ 39.1km x 19.5km + /// 5 ≤ 4.89km x 4.89km + /// 6 ≤ 1.22km x 0.61km + /// 7 ≤ 153m x 153m + /// 8 ≤ 38.2m x 19.1m + /// 9 ≤ 4.77m x 4.77m + /// 10 ≤ 1.19m x 0.596m + /// 11 ≤ 149mm x 149mm + /// 12 ≤ 37.2mm x 18.6mm + /// ``` + public var precision: Int { + return geohash.count } - // - MARK: Private - static func geohashbox(latitude: Double, longitude: Double, _ precision: Int = Geohash.defaultPrecision) -> GeohashBox? { + // MARK: - Private properties + let north: Double + let west: Double + let south: Double + let east: Double + + // MARK: - Initializers + + /// Creates a geohash based on the provided coordinates and the requested precision. + /// - parameter coordinates: The coordinates to use for generating the hash. + /// - parameter precision: The number of characters to generate. + /// ``` + /// Precision Cell width Cell height + /// 1 ≤ 5,000km x 5,000km + /// 2 ≤ 1,250km x 625km + /// 3 ≤ 156km x 156km + /// 4 ≤ 39.1km x 19.5km + /// 5 ≤ 4.89km x 4.89km + /// 6 ≤ 1.22km x 0.61km + /// 7 ≤ 153m x 153m + /// 8 ≤ 38.2m x 19.1m + /// 9 ≤ 4.77m x 4.77m + /// 10 ≤ 1.19m x 0.596m + /// 11 ≤ 149mm x 149mm + /// 12 ≤ 37.2mm x 18.6mm + /// ``` + /// - returns: If the specified coordinates are invalid, this returns nil. + public init?(coordinates: Coordinates, precision: Int = Geohash.defaultPrecision) { var lat = (-90.0, 90.0) var lon = (-180.0, 180.0) // to be generated result. - var geohash = String() + var generatedHash = Hash() // Loop helpers var parity_mode = Parity.even; var base32char = 0 - var bit = Base32BitflowInit + var bit = Geohash.Base32BitflowInit repeat { switch (parity_mode) { case .even: let mid = (lon.0 + lon.1) / 2 - if(longitude >= mid) { + if (coordinates.longitude >= mid) { base32char |= Int(bit) lon.0 = mid; } else { @@ -77,7 +145,7 @@ public struct Geohash { } case .odd: let mid = (lat.0 + lat.1) / 2 - if(latitude >= mid) { + if(coordinates.latitude >= mid) { base32char |= Int(bit) lat.0 = mid; } else { @@ -91,28 +159,36 @@ public struct Geohash { bit >>= 1 if(bit == 0b00000) { - geohash += String(DecimalToBase32Map[base32char]) - bit = Base32BitflowInit // set next character round. + generatedHash += Hash(Geohash.DecimalToBase32Map[base32char]) + bit = Geohash.Base32BitflowInit // set next character round. base32char = 0 } - } while geohash.count < precision + } while generatedHash.count < precision - return GeohashBox(hash: geohash, north: lat.1, west: lon.0, south: lat.0, east: lon.1) + self.north = lat.1 + self.west = lon.0 + self.south = lat.0 + self.east = lon.1 + + self.geohash = generatedHash } - static func geohashbox(_ hash: String) -> GeohashBox? { - var parity_mode = Parity.even; + /// Try to create a geohash based on an existing hash. Useful for finding the center coordinate of the hash. + /// - parameter hash: The existing hash to reverse hash. + /// - returns: If the provided `hash` is invalid, this will return `nil`. + public init?(geohash hash: Hash) { + var parity_mode = Parity.even var lat = (-90.0, 90.0) var lon = (-180.0, 180.0) for c in hash { - guard let bitmap = DecimalToBase32Map.firstIndex(of: c) else { + guard let bitmap = Geohash.DecimalToBase32Map.firstIndex(of: c) else { // Break on non geohash code char. return nil } - var mask = Int(Base32BitflowInit) + var mask = Int(Geohash.Base32BitflowInit) while mask != 0 { switch (parity_mode) { @@ -135,26 +211,75 @@ public struct Geohash { } } - return GeohashBox(hash: hash, north: lat.1, west: lon.0, south: lat.0, east: lon.1) + self.north = lat.1 + self.west = lon.0 + self.south = lat.0 + self.east = lon.1 + + self.geohash = hash } - static func neighbor(_ box: GeohashBox?, direction: CompassPoint, precision: Int) -> GeohashBox? { - guard let box = box else { return nil } + // MARK: - Neighbors + public struct Neighbors { + public let origin: Geohash + public let north: Geohash + public let northeast: Geohash + public let east: Geohash + public let southeast: Geohash + public let south: Geohash + public let southwest: Geohash + public let west: Geohash + public let northwest: Geohash - switch (direction) { + /// The neighboring geohashes sorted by compass direction in clockwise starting with `North`. + public var all: [Geohash] { + return [ + north, + northeast, + east, + southeast, + south, + southwest, + west, + northwest + ] + } + } + + /// - returns: The neighboring geohashes. + public var neighbors: Neighbors? { + guard + let n = neighbor(direction: .north), // N + let s = neighbor(direction: .south), // S + let e = neighbor(direction: .east), // E + let w = neighbor(direction: .west), // W + let ne = n.neighbor(direction: .east), // NE + let nw = n.neighbor(direction: .west), // NW + let se = s.neighbor(direction: .east), // SE + let sw = s.neighbor(direction: .west) // SW + else { return nil } + + return Neighbors(origin: self, north: n, northeast: ne, east: e, southeast: se, south: s, southwest: sw, west: w, northwest: nw) + } + + func neighbor(direction: CompassPoint) -> Geohash? { + let latitude: Double + let longitude: Double + switch direction { case .north: - let new_latitude = box.point.latitude + box.size.latitude // North is upper in the latitude scale - return geohashbox(latitude: new_latitude, longitude: box.point.longitude, precision) + latitude = self.latitude + self.size.latitude // North is upper in the latitude scale + longitude = self.longitude case .south: - let new_latitude = box.point.latitude - box.size.latitude // South is lower in the latitude scale - return geohashbox(latitude: new_latitude, longitude: box.point.longitude, precision) + latitude = self.latitude - self.size.latitude // South is lower in the latitude scale + longitude = self.longitude case .east: - let new_longitude = box.point.longitude + box.size.longitude // East is bigger in the longitude scale - return geohashbox(latitude: box.point.latitude, longitude: new_longitude, precision) + latitude = self.latitude + longitude = self.longitude + self.size.longitude // East is bigger in the longitude scale case .west: - let new_longitude = box.point.longitude - box.size.longitude // West is lower in the longitude scale - return geohashbox(latitude: box.point.latitude, longitude: new_longitude, precision) + latitude = self.latitude + longitude = self.longitude - self.size.longitude // West is lower in the longitude scale } - } + return Geohash(coordinates: (latitude, longitude), precision: self.precision) + } } diff --git a/Sources/GeohashKit/GeohashBox.swift b/Sources/GeohashKit/GeohashBox.swift deleted file mode 100644 index dcd8991..0000000 --- a/Sources/GeohashKit/GeohashBox.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// GeohashBox.swift -// Original by Maxim Veksler. Redistributed under MIT license. -// - -struct GeohashBox { - let hash: String - - let north: Double // top latitude - let west: Double // left longitude - let south: Double // bottom latitude - let east: Double // right longitude - var point: (latitude: Double, longitude: Double) { - let latitude = (self.north + self.south) / 2 - let longitude = (self.east + self.west) / 2 - - return (latitude: latitude, longitude: longitude) - } - - var size: (latitude: Double, longitude: Double) { - // * possible case examples: - // - // 1. bbox.north = 60, bbox.south = 40; point.latitude = 50, size.latitude = 20 ✅ - // 2. bbox.north = -40, bbox.south = -60; point.latitude = -50, size.latitude = 20 ✅ - // 3. bbox.north = 10, bbox.south = -10; point.latitude = 0, size.latitude = 20 ✅ - let latitude = north - south - - // * possible case examples: - // - // 1. bbox.east = 60, bbox.west = 40; point.longitude = 50, size.longitude = 20 ✅ - // 2. bbox.east = -40, bbox.west = -60; point.longitude = -50, size.longitude = 20 ✅ - // 3. bbox.east = 10, bbox.west = -10; point.longitude = 0, size.longitude = 20 ✅ - let longitude = east - west - - - return (latitude: latitude, longitude: longitude) - } -} diff --git a/Sources/GeohashKit/MapKit/Geohash+MapKit.swift b/Sources/GeohashKit/MapKit/Geohash+MapKit.swift new file mode 100644 index 0000000..01997e3 --- /dev/null +++ b/Sources/GeohashKit/MapKit/Geohash+MapKit.swift @@ -0,0 +1,51 @@ +// +// Geohash+MapKit.swift +// +// Created by Alan Chu on 8/2/20. +// + +#if canImport(MapKit) +import MapKit + +extension Geohash { + /// The geohash cell expressed as an `MKCoordinateRegion`. + public var region: MKCoordinateRegion { + let coordinates = CLLocationCoordinate2D(latitude: self.latitude, longitude: self.longitude) + let size = self.size + + let span = MKCoordinateSpan(latitudeDelta: size.latitude, + longitudeDelta: size.longitude) + + return MKCoordinateRegion(center: coordinates, span: span) + } +} +#endif + +#if canImport(CoreLocation) +import CoreLocation + +extension Geohash { + /// Creates a geohash based on the provided coordinates and the requested precision. + /// - parameter coordinates: The coordinates to use for generating the hash. + /// - parameter precision: The number of characters to generate. + /// ``` + /// Precision Cell width Cell height + /// 1 ≤ 5,000km x 5,000km + /// 2 ≤ 1,250km x 625km + /// 3 ≤ 156km x 156km + /// 4 ≤ 39.1km x 19.5km + /// 5 ≤ 4.89km x 4.89km + /// 6 ≤ 1.22km x 0.61km + /// 7 ≤ 153m x 153m + /// 8 ≤ 38.2m x 19.1m + /// 9 ≤ 4.77m x 4.77m + /// 10 ≤ 1.19m x 0.596m + /// 11 ≤ 149mm x 149mm + /// 12 ≤ 37.2mm x 18.6mm + /// ``` + /// - returns: If the specified coordinates are invalid, this returns nil. + public init?(_ coordinates: CLLocationCoordinate2D, precision: Int) { + self.init(coordinates: (coordinates.latitude, coordinates.longitude), precision: precision) + } +} +#endif diff --git a/Tests/GeohashKitTests/GeohashKitTests.swift b/Tests/GeohashKitTests/GeohashKitTests.swift index 7801bf2..46f30f7 100644 --- a/Tests/GeohashKitTests/GeohashKitTests.swift +++ b/Tests/GeohashKitTests/GeohashKitTests.swift @@ -5,21 +5,21 @@ final class GeohashKitTests: XCTestCase { // - MARK: encode func testEncode() { // geohash.org - XCTAssertEqual(Geohash.encode(latitude: -25.383, longitude: -49.266, 8), "6gkzwgjt") - XCTAssertEqual(Geohash.encode(latitude: -25.382708, longitude: -49.265506, 12), "6gkzwgjzn820") - XCTAssertEqual(Geohash.encode(latitude: -25.427, longitude: -49.315, 8), "6gkzmg1u") + XCTAssertEqual(Geohash(coordinates: (-25.383, -49.266), precision: 8)?.geohash, "6gkzwgjt") + XCTAssertEqual(Geohash(coordinates: (-25.382708, -49.265506), precision: 12)?.geohash, "6gkzwgjzn820") + XCTAssertEqual(Geohash(coordinates: (-25.427, -49.315), precision: 8)?.geohash, "6gkzmg1u") // Geohash Tool - XCTAssertEqual(Geohash.encode(latitude: -31.953, longitude: 115.857, 8), "qd66hrhk") - XCTAssertEqual(Geohash.encode(latitude: 38.89710201881826, longitude: -77.03669792041183, 12), "dqcjqcp84c6e") + XCTAssertEqual(Geohash(coordinates: (-31.953, 115.857), precision: 8)?.geohash, "qd66hrhk") + XCTAssertEqual(Geohash(coordinates: (38.89710201881826, -77.03669792041183), precision: 12)?.geohash, "dqcjqcp84c6e") // Narrow samples. - XCTAssertEqual(Geohash.encode(latitude: 42.6, longitude: -5.6, 5), "ezs42") + XCTAssertEqual(Geohash(coordinates: (42.6, -5.6), precision: 5)?.geohash, "ezs42") } func testEncodeDefaultPrecision() { // Narrow samples. - XCTAssertEqual(Geohash.encode(latitude: 42.6, longitude: -5.6), "ezs42") + XCTAssertEqual(Geohash(coordinates: (42.6, -5.6))?.geohash, "ezs42") // XCTAssertEqual(Geohash.encode(latitude: 0, longitude: 0), "s000") // => "s0000" :( hopefully will be resovled by #Issue:1 } @@ -27,10 +27,10 @@ final class GeohashKitTests: XCTestCase { // - MARK: decode /// Testing latitude & longitude decode correctness, with epsilon precision. func aDecodeUnitTest(_ hash: String, _ expectedLatitude: Double, _ expectedLongitude: Double) { - let (latitude, longitude) = Geohash.decode(hash)!; + let geohash = Geohash(geohash: hash)! - XCTAssertEqual(latitude, expectedLatitude, accuracy: Double(Float.ulpOfOne)) - XCTAssertEqual(longitude, expectedLongitude, accuracy: Double(Float.ulpOfOne)) + XCTAssertEqual(geohash.latitude, expectedLatitude, accuracy: Double(Float.ulpOfOne)) + XCTAssertEqual(geohash.longitude, expectedLongitude, accuracy: Double(Float.ulpOfOne)) } func testDecode() { @@ -38,16 +38,25 @@ final class GeohashKitTests: XCTestCase { aDecodeUnitTest("spey61y", 43.296432495117, 5.3702545166016) } + func compareNeighbors(origin: String, expectedNeighbors: [String]) { + let geohash = Geohash(geohash: origin)! + let originNeighbors = geohash.neighbors?.all.map { $0.geohash } + XCTAssertEqual(originNeighbors, expectedNeighbors) + } + // - MARK: neighbors func testNeighbors() { // Bugrashov, Tel Aviv, Israel - XCTAssertEqual(["sv8wrqfq", "sv8wrqfw", "sv8wrqft", "sv8wrqfs", "sv8wrqfk", "sv8wrqfh", "sv8wrqfj", "sv8wrqfn"], Geohash.neighbors("sv8wrqfm")!) + compareNeighbors(origin: "sv8wrqfm", expectedNeighbors: ["sv8wrqfq", "sv8wrqfw", "sv8wrqft", "sv8wrqfs", "sv8wrqfk", "sv8wrqfh", "sv8wrqfj", "sv8wrqfn"]) + // Meridian Gardens - XCTAssertEqual(["gcpvpbpbp", "u10j00000", "u10hbpbpb", "u10hbpbp8", "gcpuzzzzx", "gcpuzzzzw", "gcpuzzzzy", "gcpvpbpbn"], Geohash.neighbors("gcpuzzzzz")!) + compareNeighbors(origin: "gcpuzzzzz", expectedNeighbors: ["gcpvpbpbp", "u10j00000", "u10hbpbpb", "u10hbpbp8", "gcpuzzzzx", "gcpuzzzzw", "gcpuzzzzy", "gcpvpbpbn"]) + // Overkills are fun! - XCTAssertEqual(["cbsuv7ztq4345234323d", "cbsuv7ztq4345234323f", "cbsuv7ztq4345234323c", "cbsuv7ztq4345234323b", "cbsuv7ztq43452343238", "cbsuv7ztq43452343232", "cbsuv7ztq43452343233", "cbsuv7ztq43452343236"], Geohash.neighbors("cbsuv7ztq43452343239")!) + compareNeighbors(origin: "cbsuv7ztq43452343239", expectedNeighbors: ["cbsuv7ztq4345234323d", "cbsuv7ztq4345234323f", "cbsuv7ztq4345234323c", "cbsuv7ztq4345234323b", "cbsuv7ztq43452343238", "cbsuv7ztq43452343232", "cbsuv7ztq43452343233", "cbsuv7ztq43452343236"]) + // France - XCTAssertEqual(["u001", "u003", "u002", "spbr", "spbp", "ezzz", "gbpb", "gbpc"], Geohash.neighbors("u000")!) + compareNeighbors(origin: "u000", expectedNeighbors: ["u001", "u003", "u002", "spbr", "spbp", "ezzz", "gbpb", "gbpc"]) } static var allTests = [