Skip to content

Instantly share code, notes, and snippets.

@a-voronov
Last active September 15, 2021 16:58
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save a-voronov/c5c9b22c58e2347d941de0ed95535b57 to your computer and use it in GitHub Desktop.
Save a-voronov/c5c9b22c58e2347d941de0ed95535b57 to your computer and use it in GitHub Desktop.
proximity-hash
import CoreLocation
/// Geohash algorithm implementation.
///
/// It is a hierarchical spatial data structure which subdivides space into buckets of grid shape,
/// which is one of the many applications of what is known as a Z-order curve, and generally space-filling curves.
///
/// Geohashes offer properties like arbitrary precision
/// and the possibility of gradually removing characters from the end of the code to reduce its size (and gradually lose precision).
/// Geohashing guarantees that the longer a shared prefix between two geohashes is, the spatially closer they are together.
/// The reverse of this is not guaranteed, as two points can be very close but have a short or no shared prefix.
///
/// Links:
/// - [Wikipedia](https://en.wikipedia.org/wiki/Geohash)
/// - [Geohash.org](http://geohash.org)
/// - [Live Map](http://mapzen.github.io/leaflet-spatial-prefix-tree/)
/// - [JS](http://www.movable-type.co.uk/scripts/geohash.html) and [Python](https://github.com/wdm0006/pygeohash) implementations
public enum GeoHash {
// bit parity, used while encoding/decoding geohash
private enum Parity: Equatable {
case even, odd
mutating func toggle() {
switch self {
case .even: self = .odd
case .odd: self = .even
}
}
}
// geohash-specific base32 alphabet for encoding/decoding
private static let base32EncodeTable = Array("0123456789bcdefghjkmnpqrstuvwxyz")
private static let base32DecodeTable = base32EncodeTable.charAtIdxTable
// each character in geohash-specific base32 alphabet is 5 bits long
private static let base32CharFirstBit = 0b10000
private static let base32CharLastBit = 0b00000
// valid bounds of the world
static let world = (
lat: (min: -90.0, max: 90.0),
lon: (min: -180.0, max: 180.0)
)
// minimal and maximal geohash lengths allowed
private static let geohashLengthBounds = (min: 1, max: 22)
// precalculated tables of neigbours for the character at respective position
private static let neighborsEncodeTable: [Direction: [Parity: [String.Element]]] = [
.north: [.even: Array("p0r21436x8zb9dcf5h7kjnmqesgutwvy"), .odd: Array("bc01fg45238967deuvhjyznpkmstqrwx")],
.south: [.even: Array("14365h7k9dcfesgujnmqp0r2twvyx8zb"), .odd: Array("238967debc01fg45kmstqrwxuvhjyznp")],
.east: [.even: Array("bc01fg45238967deuvhjyznpkmstqrwx"), .odd: Array("p0r21436x8zb9dcf5h7kjnmqesgutwvy")],
.west: [.even: Array("238967debc01fg45kmstqrwxuvhjyznp"), .odd: Array("14365h7k9dcfesgujnmqp0r2twvyx8zb")]
]
private static let neighborsDecodeTable = neighborsEncodeTable.compactMapValues { neigborsPerParity in
zip(neigborsPerParity[.even], neigborsPerParity[.odd]).map { even, odd in
[Parity.even: even.charAtIdxTable, .odd: odd.charAtIdxTable]
}
}
// precalculated tables of borders for the grid
private static let bordersEncodeTable: [Direction: [Parity: [String.Element]]] = [
.north: [.even: Array("prxz"), .odd: Array("bcfguvyz")],
.south: [.even: Array("028b"), .odd: Array("0145hjnp")],
.east: [.even: Array("bcfguvyz"), .odd: Array("prxz")],
.west: [.even: Array("0145hjnp"), .odd: Array("028b")]
]
private static let bordersDecodeTable = bordersEncodeTable.compactMapValues { neigborsPerParity in
zip(neigborsPerParity[.even], neigborsPerParity[.odd]).map { even, odd in
[Parity.even: even.charAtIdxTable, .odd: odd.charAtIdxTable]
}
}
/// Encodes a coordinate to a geohash string of a given length.
///
/// If `length` is out of minimal and maximal bounds, it will be constrained to the closest one.
/// - Parameters:
/// - coordinate: coordinate to encode.
/// - length: min: 1, max: 22
/// - Returns: geohash string of a given length.
///
/// Example:
/// ```
/// let geohash = GeoHash.encode(
/// coordinate: .init(latitude: 40.75798, longitude: -73.991516),
/// length: 12
/// )
/// print(geohash) // will print "dr5ru7c02wnv"
/// ```
public static func encode(coordinate: CLLocationCoordinate2D, length: Int) -> String {
if let geohash = table[Key(coordinate: coordinate, length: length)] {
print(geohash)
return geohash
}
let geohash = Box(coordinate: coordinate, length: length).geohash
table[Key(coordinate: coordinate, length: length)] = geohash
return geohash
}
static var table = [Key: String]()
struct Key: Hashable {
let coordinate: CLLocationCoordinate2D, length: Int
}
/// Decodes given geohash string to a coordinate with the corresponding floating point precision.
///
/// Will return nil if string is invalid.
///
/// Valid geohash string should consist of the characters from this set: `0123456789bcdefghjkmnpqrstuvwxyz`
///
/// Example:
/// ```
/// let coord = GeoHash.decode(geohash: "dr5ru7c02wnv")
/// // CLLocationCoordinate2D(latitude: 40.75798, longitude: -73.991516)
/// ```
public static func decode(geohash: String) -> CLLocationCoordinate2D? {
guard let box = Box(geohash: geohash) else {
return nil
}
let center = box.center
// calculate correct precision for the center coordinate
let latPrecisionMult = pow(10, max(1, (-log(box.northEast.latitude - box.southWest.latitude) / M_LN10).rounded(.down)))
let lonPrecisionMult = pow(10, max(1, (-log(box.northEast.longitude - box.southWest.longitude) / M_LN10).rounded(.down)))
// fail if result is invalid
return latPrecisionMult.isInfinite || latPrecisionMult.isNaN || lonPrecisionMult.isInfinite || lonPrecisionMult.isNaN
? nil
: CLLocationCoordinate2D(
latitude: (center.latitude * latPrecisionMult).rounded() / latPrecisionMult,
longitude: (center.longitude * lonPrecisionMult).rounded() / lonPrecisionMult
)
}
public enum Direction: Equatable {
case north, east, west, south
}
/// Represents a bounding box for a given geohash.
///
/// Can be constructed by either providing a coordinate with required geohash length, or by providing a geohash string.
/// Contains geohash string and four vertices of its bounding box.
public struct Box: Equatable {
public let geohash: String
public let northEast: CLLocationCoordinate2D
public let southWest: CLLocationCoordinate2D
public var northWest: CLLocationCoordinate2D { return .init(latitude: northEast.latitude, longitude: southWest.longitude) }
public var southEast: CLLocationCoordinate2D { return .init(latitude: southWest.latitude, longitude: northEast.longitude) }
public var center: CLLocationCoordinate2D {
return CLLocationCoordinate2D(
latitude: (southWest.latitude + northEast.latitude) / 2,
longitude: (southWest.longitude + northEast.longitude) / 2
)
}
/// Provides 4 vertice coordinates in a clockwise direction starting with `ne`.
public var vertices: [CLLocationCoordinate2D] { return [northEast, southEast, southWest, northWest] }
/// Construct a box by providing a coordinate and required geohash string length between 1 and 22.
public init(coordinate: CLLocationCoordinate2D, length: Int) {
// correct inputs to not fail encoding
let coord = CLLocationCoordinate2D(
latitude: coordinate.latitude.inRange(min: world.lat.min, max: world.lat.max),
longitude: coordinate.longitude.inRange(min: world.lon.min, max: world.lon.max)
)
let length = length.inRange(min: geohashLengthBounds.min, max: geohashLengthBounds.max)
var (lat, lon) = world
var geohash = ""
var charIdx = 0
var bit = base32CharFirstBit
var bitParity = Parity.even
// construct geohash string of length defined by a length number,
// by searching corresponding cell in a grid while zooming in to a given length,
// and calculating & storing longitude char per every even bit, and latitude char per every odd bit in bitmask
while geohash.count < length {
if bitParity == .even {
let lonMid = (lon.min + lon.max) / 2
if coord.longitude >= lonMid {
charIdx |= bit
lon.min = lonMid
} else {
lon.max = lonMid
}
} else {
let latMid = (lat.min + lat.max) / 2
if coord.latitude >= latMid {
charIdx |= bit
lat.min = latMid
} else {
lat.max = latMid
}
}
// shift to the next bit
bit >>= 1
bitParity.toggle()
// collect character and start over once reached end
if bit == base32CharLastBit {
geohash.append(base32EncodeTable[charIdx])
bit = base32CharFirstBit
charIdx = 0
}
}
self.northEast = CLLocationCoordinate2D(latitude: lat.max, longitude: lon.max)
self.southWest = CLLocationCoordinate2D(latitude: lat.min, longitude: lon.min)
self.geohash = geohash
}
public init?(geohash: String) {
guard !geohash.isEmpty else {
return nil
}
var (lat, lon) = world
var bitParity = Parity.even
for char in geohash {
guard let charIdx = base32DecodeTable[char] else {
return nil
}
var bit = base32CharFirstBit
while bit != base32CharLastBit {
let bitN = charIdx & bit
if bitParity == .even {
let lonMid = (lon.min + lon.max) / 2
if bitN != 0 {
lon.min = lonMid
} else {
lon.max = lonMid
}
} else {
let latMid = (lat.min + lat.max) / 2
if bitN != 0 {
lat.min = latMid
} else {
lat.max = latMid
}
}
// shift to the next bit
bit >>= 1
bitParity.toggle()
}
}
self.northEast = CLLocationCoordinate2D(latitude: lat.max, longitude: lon.max)
self.southWest = CLLocationCoordinate2D(latitude: lat.min, longitude: lon.min)
self.geohash = geohash
}
}
}
// MARK: - Neighbors
public typealias GeoHashNeighbors = GeoHash.Neighbors<String>
extension GeoHash {
public struct Neighbors<T> {
public let north: T, east: T, south: T, west: T
public let northEast: T, southEast: T, southWest: T, northWest: T
/// Provides all neighbors as an array, in a clockwise direction starting with `n`.
public var all: [T] {
[north, northEast, east, southEast, south, southWest, west, northWest]
}
}
/// Provides an adjacent in specified direction for a rect by a given geohash.
public static func adjacent(geohash: String, direction: Direction) -> String? {
guard let lastChar = geohash.last else {
return nil
}
var parent: String? = String(geohash.dropLast())
let parity = geohash.count.isMultiple(of: 2) ? Parity.even : .odd
// in case they don't share common prefix
if bordersDecodeTable[direction]?[parity]?[lastChar] != nil {
parent = parent.flatMap { adjacent(geohash: $0, direction: direction) }
}
// append adjacent character by its position in original alphabet
return zip(parent, neighborsDecodeTable[direction]?[parity]?[lastChar].map { base32EncodeTable[$0] })
.map { parent, adjacentChar in
var parent = parent
parent.append(adjacentChar)
return parent
}
}
/// Provides all 8 neighboring geohashes for a given geohash.
public static func neighbors(geohash: String) -> GeoHashNeighbors? {
return Neighbors<String>(geohash: geohash)
}
}
extension GeoHash.Neighbors: Equatable where T: Equatable {}
extension GeoHashNeighbors {
public init?(geohash: String) {
guard
let north = GeoHash.adjacent(geohash: geohash, direction: .north),
let east = GeoHash.adjacent(geohash: geohash, direction: .east),
let south = GeoHash.adjacent(geohash: geohash, direction: .south),
let west = GeoHash.adjacent(geohash: geohash, direction: .west),
let northEast = GeoHash.adjacent(geohash: north, direction: .east),
let southEast = GeoHash.adjacent(geohash: south, direction: .east),
let southWest = GeoHash.adjacent(geohash: south, direction: .west),
let northWest = GeoHash.adjacent(geohash: north, direction: .west)
else {
return nil
}
self.north = north
self.east = east
self.south = south
self.west = west
self.northEast = northEast
self.southEast = southEast
self.southWest = southWest
self.northWest = northWest
}
}
// MARK: - Utils
private extension Array where Element == String.Element {
/// Constructs a character per its index table out of array.
var charAtIdxTable: [String.Element: Int] {
enumerated().reduce(into: [:]) { acc, charAtIdx in
acc[charAtIdx.element] = charAtIdx.offset
}
}
}
import CoreLocation
import MapKit
/// ProximityHash generates a set of geohashes that cover a circular area,
/// given the center coordinates, the radius and the required length of geohash.
///
/// Based on [proximityhash](https://github.com/ashwin711/proximityhash) .
/// However, instead of operating grid cells dimensions using metric distance units (m/km), which change from equator to the poles,
/// we operate with degrees deltas that are always the same. This gives us exactly precise results, unlike original version.
public enum ProximityHash {
private static let geohashLengthBounds = (min: 1, max: 12)
private static let minCellsToCheck = 2
// precalculted spans for grid cells at given precision (1-12),
// they're always of the same values no matter what location you need, whether close to equator or close to the poles.
private static let gridSpans = [
MKCoordinateSpan(latitudeDelta: 45.0, longitudeDelta: 45.0),
MKCoordinateSpan(latitudeDelta: 5.625, longitudeDelta: 11.25),
MKCoordinateSpan(latitudeDelta: 1.40625, longitudeDelta: 1.40625),
MKCoordinateSpan(latitudeDelta: 0.17578125, longitudeDelta: 0.3515625),
MKCoordinateSpan(latitudeDelta: 0.0439453125, longitudeDelta: 0.0439453125),
MKCoordinateSpan(latitudeDelta: 0.0054931640625, longitudeDelta: 0.010986328125),
MKCoordinateSpan(latitudeDelta: 0.001373291015625, longitudeDelta: 0.001373291015625),
MKCoordinateSpan(latitudeDelta: 0.000171661376953125, longitudeDelta: 0.00034332275390625),
MKCoordinateSpan(latitudeDelta: 4.291534423828125e-05, longitudeDelta: 4.291534423828125e-05),
MKCoordinateSpan(latitudeDelta: 5.364418029785156e-06, longitudeDelta: 1.0728836059570312e-05),
MKCoordinateSpan(latitudeDelta: 1.341104507446289e-06, longitudeDelta: 1.341104507446289e-06),
MKCoordinateSpan(latitudeDelta: 1.6763806343078613e-07, longitudeDelta: 3.3527612686157227e-07)
]
/// Represents the strategy of constructing geohashes.
public enum Inclusion: Equatable {
/// Will include geohashes that intersect with the circle area even a bit.
case bounding
/// Will include geohashes which are only inside the circle area 100%.
case bounded
}
/// Generates a set of geohashes that approximate a circle.
///
/// There will always be at least center point's geohash,
/// no matter what inclusion is and how small the circle area is, even if it's smaller than the geohash cell.
///
/// - Parameters:
/// - center: center coordinate (origin of the circle area)
/// - radius: radius in meters
/// - length: length of geohash strings from 1 to 12. The higher the number, the small the cells will be
/// - inclusion:
/// a choice to either get fully bound geohashes inside a circle are,
/// or to keep bounding geohashes that intersect circle area even a bit
/// - Returns:
/// - center:
/// - geohashes: unqiue set of geohash strings
public static func geohashes(
aroundCoordinate center: CLLocationCoordinate2D,
inRadius radius: CLLocationDistance,
ofLength length: Int,
inclusion: Inclusion = .bounding
) -> Set<String> {
var points = Set<CLLocationCoordinate2D>()
var geohashes = Set<String>()
// keep length in supported range
let length = length.inRange(min: geohashLengthBounds.min, max: geohashLengthBounds.max)
let span = gridSpans[length - 1]
// constructing a bounding rect from radius in metrtic points to get its span deltas in degrees
let circleBoundingRect = MKCoordinateRegion(center: center, latitudinalMeters: radius * 2, longitudinalMeters: radius * 2)
let circleLatSpan = min(circleBoundingRect.span.latitudeDelta, GeoHash.world.lat.max)
let circleLonSpan = min(circleBoundingRect.span.longitudeDelta, GeoHash.world.lon.max)
let (circleLatRadius, circleLonRadius) = (circleLatSpan / 2, circleLonSpan / 2)
// calculating number of cells inside this rect in vertical and horizontal directions
let latCells = max(minCellsToCheck, Int((circleLatSpan / span.latitudeDelta).rounded(.up)))
let lonCells = max(minCellsToCheck, Int((circleLonSpan / span.longitudeDelta).rounded(.up)))
// creating a circular region from radius in metrtic points,
// to help figuring out whether given coordinate is inside of it during future calculations
let circle = CLCircularRegion(
center: center,
radius: radius,
identifier: "ProximityHash.geohashes(c:\(center),r:\(radius),l:\(length))"
)
// calculating center of the geohash bounding box for the given area center,
// to help precisely move from one box to another using span deltas for the given precision
let bbox = GeoHash.Box(coordinate: center, length: length)
let origin = bbox.center
// for bounded inclusion, no need to perform any extra work if circle area is smaller than the cell
if inclusion == .bounded && (span.latitudeDelta >= circleLatSpan || span.longitudeDelta >= circleLonSpan) {
return [bbox.geohash]
}
// we start moving from the center of the area in the north-east direction,
// and for each such move we generate 4 points equally-distanced from the center in all 4 directions (ne, se, sw, nw).
// this way we can work within each sector of the circle area and reduce cost of checking if cell falls into it.
for latCellIdx in 0 ..< latCells {
let latDiff = span.latitudeDelta * Double(latCellIdx)
// all coordinates' calculations should respect max and min coordinates on the map to wrap their values around them
// i.e. we can't have longitude greater than 180 or less than -180,
// and if we do, we should move remainder to the 'other side', so that 185 would become -175, and same for longitude.
let northLat = corrected(lat: origin.latitude + latDiff)
let southLat = corrected(lat: origin.latitude - latDiff)
for lonCellIdx in 0 ..< lonCells {
let lonDiff = span.longitudeDelta * Double(lonCellIdx)
let eastLon = corrected(lon: origin.longitude + lonDiff)
let westLon = corrected(lon: origin.longitude - lonDiff)
// constructing 4 cells - 1 for each sector of the circle area
let nw = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: northLat, longitude: westLon), span: span)
let ne = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: northLat, longitude: eastLon), span: span)
let se = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: southLat, longitude: eastLon), span: span)
let sw = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: southLat, longitude: westLon), span: span)
switch inclusion {
case .bounding:
if lonCellIdx == 0 {
// some hacks to avoid even more complex calculations:
//
// the only cells that can intersect circle area, without having any of their vertices inside that area,
// is when cells intersect it with their edges, yet not too far to intersect it with any of their vertex.
// i.e. like this:
// ___
// _/ \_ +–––––+
// / \| |
// ( |) |
// \_ _/| |
// \___/ +–––––+
//
// this can happen to only 4 cells, coming from the north, south, east and west,
// that's why we check each cell only in 0 latitude and 0 longitude positions for exactly this case.
//
// thus, in longitude 0 position, we check each cell's latitude edge which is closer to the center,
// and if that edge is closer to the center than circle's latitude radius, then it intersects that area.
//
// same is true for the longitude direction, when latitude cell position is 0
// and its longitude edge is closer to the center than circle's longitude radius delta.
if abs(corrected(lat: ne.sLat - center.latitude)) < circleLatRadius { points.insert(ne.center) }
if abs(corrected(lat: nw.sLat - center.latitude)) < circleLatRadius { points.insert(nw.center) }
if abs(corrected(lat: center.latitude - se.nLat)) < circleLatRadius { points.insert(se.center) }
if abs(corrected(lat: center.latitude - sw.nLat)) < circleLatRadius { points.insert(sw.center) }
} else if latCellIdx == 0 {
if abs(corrected(lon: nw.eLon - center.longitude)) < circleLonRadius { points.insert(nw.center) }
if abs(corrected(lon: sw.eLon - center.longitude)) < circleLonRadius { points.insert(sw.center) }
if abs(corrected(lon: center.longitude - ne.wLon)) < circleLonRadius { points.insert(ne.center) }
if abs(corrected(lon: center.longitude - se.wLon)) < circleLonRadius { points.insert(se.center) }
} else {
// this is the most common case - for each cell in each of 4 sectors,
// we check if that cell's closest point to the circle center is contained in the circle area.
// i.e. for the cell in `ne` sector, point to check would be `sw`
if circle.contains(nw.se) { points.insert(nw.center) }
if circle.contains(ne.sw) { points.insert(ne.center) }
if circle.contains(se.nw) { points.insert(se.center) }
if circle.contains(sw.ne) { points.insert(sw.center) }
}
case .bounded:
// if requesting only bounded cells, apply same strategy - make exceptional checks for 0 lat and 0 lon cells
// and check if both furthest from the center vertices are inside the circle area.
if lonCellIdx == 0 {
if circle.contains(nw.nw) && circle.contains(nw.ne) { points.insert(nw.center) }
if circle.contains(ne.ne) && circle.contains(ne.nw) { points.insert(ne.center) }
if circle.contains(se.se) && circle.contains(se.sw) { points.insert(se.center) }
if circle.contains(sw.sw) && circle.contains(sw.se) { points.insert(sw.center) }
} else if latCellIdx == 0 {
if circle.contains(nw.nw) && circle.contains(nw.sw) { points.insert(nw.center) }
if circle.contains(ne.ne) && circle.contains(ne.se) { points.insert(ne.center) }
if circle.contains(se.se) && circle.contains(se.ne) { points.insert(se.center) }
if circle.contains(sw.sw) && circle.contains(sw.nw) { points.insert(sw.center) }
} else {
// this is the most common case - for each cell in each of 4 sectors,
// we check if that cell's furthest point from the circle center is contained in the circle area.
// i.e. for the cell in `ne` sector, point to check would be `ne`
//
// basically inverted from what we did with `bounding` strategy,
// and it is cheaper than checking if all four vertices fall in a circle area, but gives same result :)
if circle.contains(nw.nw) { points.insert(nw.center) }
if circle.contains(ne.ne) { points.insert(ne.center) }
if circle.contains(se.se) { points.insert(se.center) }
if circle.contains(sw.sw) { points.insert(sw.center) }
}
}
}
}
for point in points {
geohashes.insert(GeoHash.encode(coordinate: point, length: length))
}
// we always include coordinate's geohash in any case
geohashes.insert(bbox.geohash)
return geohashes
}
}
extension MKCoordinateRegion {
var wLon: CLLocationDegrees { return corrected(lon: center.longitude - span.longitudeDelta / 2) }
var eLon: CLLocationDegrees { return corrected(lon: center.longitude + span.longitudeDelta / 2) }
var nLat: CLLocationDegrees { return corrected(lat: center.latitude + span.latitudeDelta / 2) }
var sLat: CLLocationDegrees { return corrected(lat: center.latitude - span.latitudeDelta / 2) }
var nw: CLLocationCoordinate2D { return CLLocationCoordinate2D(latitude: nLat, longitude: wLon) }
var se: CLLocationCoordinate2D { return CLLocationCoordinate2D(latitude: sLat, longitude: eLon) }
var sw: CLLocationCoordinate2D { return CLLocationCoordinate2D(latitude: sLat, longitude: wLon) }
var ne: CLLocationCoordinate2D { return CLLocationCoordinate2D(latitude: nLat, longitude: eLon) }
var vertices: [CLLocationCoordinate2D] { return [ne, se, sw, nw] }
}
/// Corrects longitude if it gets out of bounds.
/// i.e. 183 -> -177 or -196 -> 164
private func corrected(lon: CLLocationDegrees) -> CLLocationDegrees {
if lon > GeoHash.world.lon.max {
return GeoHash.world.lon.min + (lon - GeoHash.world.lon.max)
} else if lon < GeoHash.world.lon.min {
return GeoHash.world.lon.max + (lon - GeoHash.world.lon.min)
}
return lon
}
/// Corrects latitude if it gets out of bounds.
/// i.e. 93 -> -87 or -106 -> 74
private func corrected(lat: CLLocationDegrees) -> CLLocationDegrees {
if lat > GeoHash.world.lat.max {
return GeoHash.world.lat.min + (lat - GeoHash.world.lat.max)
} else if lat < GeoHash.world.lat.min {
return GeoHash.world.lat.max + (lat - GeoHash.world.lat.min)
}
return lat
}
extension CLLocationCoordinate2D: Hashable {
public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool {
return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
}
public func hash(into hasher: inout Hasher) {
hasher.combine(latitude)
hasher.combine(longitude)
}
}
extension Comparable {
func inRange(min minValue: Self, max maxValue: Self) -> Self {
guard minValue < maxValue else { return self }
return min(max(self, minValue), maxValue)
}
}
func zip<A, B>(_ a: A?, _ b: B?) -> (A, B)? {
return a.flatMap { a in b.map { b in (a, b) } }
}
@a-voronov
Copy link
Author

a-voronov commented Jul 20, 2020

Example

let length = 6
let radius = 2500.0
let center = CLLocationCoordinate2D(latitude: 40.75798, longitude: -73.991516)

let geohash = GeoHash.encode(coordinate: center, length: length)

// "dr5ru7"

let neighbors = GeoHash.neighbors(geohash: geohash)

// north:     "dr5ruk"
// northEast: "dr5rus"
// east:      "dr5rue"
// southEast: "dr5rud"
// south:     "dr5ru6"
// southWest: "dr5ru4"
// west:      "dr5ru5"
// northWest: "dr5ruh"

let geohashes = ProximityHash.geohashes(aroundCoordinate: center, inRadius: radius, ofLength: length)

// "dr5rgu", "dr5ruu", "dr5rud", "dr5rgg", "dr5rgt", 
// "dr5rgs", "dr5rut", "dr5rgw", "dr5run", "dr5ru3", 
// "dr5rue", "dr5ruv", "dr5ru9", "dr5rur", "dr5ru5", 
// "dr5rum", "dr5rge", "dr5rup", "dr5rg9", "dr5ru6", 
// "dr5rsr", "dr5rgv", "dr5ruk", "dr5rsx", "dr5rgd", 
// "dr5ruh", "dr5ru1", "dr5rgc", "dr5ruc", "dr5rgf", 
// "dr5rsp", "dr5ruj", "dr5rux", "dr5rgb", "dr5rug", 
// "dr5ru4", "dr5rgz", "dr5ru0", "dr5ru2", "dr5ruy", 
// "dr5rub", "dr5ru8", "dr5ruw", "dr5rgy", "dr5rus", 
// "dr5ruq", "dr5ruf", "dr5ru7"

6-bounding

You can also ask to only return geohashes for b-boxes fully bounded inside the circle area:

let length = 6
let radius = 2500.0
let center = CLLocationCoordinate2D(latitude: 40.75798, longitude: -73.991516)

let geohashes = ProximityHash.geohashes(aroundCoordinate: center, inRadius: radius, ofLength: length, inclusion: .bounded)

// "dr5ru1", "dr5ru4", "dr5rgu", "dr5rum", "dr5ru5",
// "dr5ruj", "dr5ru3", "dr5ruq", "dr5ru7", "dr5ru2",
// "dr5ruh", "dr5rue", "dr5rus", "dr5rgg", "dr5ruk", 
// "dr5ru6", "dr5rgv", "dr5ru9", "dr5rud", "dr5run", 
// "dr5rut", "dr5rgf"

6-bounded

The longer hashes you want to get, the less the area they will take, and the more of them you'll get:

let length = 7
let radius = 2500.0
let center = CLLocationCoordinate2D(latitude: 40.75798, longitude: -73.991516)

let geohashes = ProximityHash.geohashes(aroundCoordinate: center, inRadius: radius, ofLength: length)

// "dr5rgvd", "dr5ru65", "dr5ru70", "dr5ruv5", "dr5ruk5", "dr5runk", "dr5rgc4", "dr5runs", "dr5rsrf", 
// "dr5ru7y", "dr5ru9j", "dr5rgdz", "dr5rudk", "dr5rgg1", "dr5ru59", "dr5rum4", "dr5rujc", "dr5rur5",
// "dr5ru21", "dr5ruhw", "dr5rgys", "dr5ru1n", "dr5rudp", "dr5ruex", "dr5rut2", "dr5ru6d", "dr5rupn", 
// "dr5rguc", "dr5rur1", "dr5runw", "dr5ruwn", "dr5run3", "dr5rukk", "dr5rux2", "dr5ruhn", "dr5ruus",
// "dr5rsrs", "dr5ru50", "dr5ru22", "dr5ruqj", "dr5ru12", "dr5rgz5", "dr5ru3u", "dr5ru53", "dr5rgcv", 
// "dr5rur7", "dr5rum6", "dr5ruwz", "dr5ru85", "dr5ruh8", "dr5rgge", "dr5rur9", "dr5rugq", "dr5ruv1",
// "dr5rgf8", "dr5rgdv", "dr5ru31", "dr5rgbz", "dr5ru9k", "dr5rus1", "dr5ruv6", "dr5rgep", "dr5rug4", 
// "dr5rggp", "dr5ru4n", "dr5ru1h", "dr5ru7p", "dr5rumh", "dr5ruu0", "dr5rgth", "dr5ru72", "dr5ru7z",
// "dr5rugk", "dr5ruy1", "dr5rgy4", "dr5ru03", "dr5ru04", "dr5rgun", "dr5ru3q", "dr5ruw4", "dr5rgcc", 
// "dr5rggj", "dr5rur4", "dr5rgft", "dr5ruj4", "dr5ruek", "dr5ruqx", "dr5rur2", "dr5ru2y", "dr5ru6w",
// "dr5ru08", "dr5ru82", "dr5rus9", "dr5ruk7", "dr5rgct", "dr5ruqb", "dr5rup1", "dr5ru2p", "dr5ru60", 
// "dr5rgbm", "dr5ruwg", "dr5rutw", "dr5ruet", "dr5ru91", "dr5rgdg", "dr5rg9x", "dr5rgy8", "dr5ru97",
// "dr5ru0f", "dr5ru6p", "dr5rgch", "dr5ruu6", "dr5ruuu", "dr5ru3x", "dr5ru1r", "dr5rumk", "dr5ruhe", 
// "dr5rgg8", "dr5rgbk", "dr5rusu", "dr5rgbd", "dr5rupr", "dr5ru5u", "dr5rgzm", "dr5ruu2", "dr5rud4",
// "dr5ru90", "dr5rguq", "dr5rutc", "dr5ruhg", "dr5rspz", "dr5runt", "dr5ru15", "dr5ruf8", "dr5ruwj", 
// "dr5ru58", "dr5rug2", "dr5rutv", "dr5ru1d", "dr5rujg", "dr5ru5j", "dr5rudj", "dr5rgu0", "dr5rgfs",
// "dr5ruuh", "dr5ruwx", "dr5ruej", "dr5ru9b", "dr5rurt", "dr5rucb", "dr5ruvc", "dr5rgdj", "dr5rgc5", 
// "dr5ru78", "dr5rggz", "dr5ru7c", "dr5rue1", "dr5rupv", "dr5rg9z", "dr5rujq", "dr5rusv", "dr5ru3t",
// "dr5ru1p", "dr5ru2m", "dr5rufy", "dr5rut3", "dr5ru01", "dr5rug7", "dr5ruwh", "dr5ruec", "dr5ruj0", 
// "dr5ruwr", "dr5ruy6", "dr5rus5", "dr5rusz", "dr5rgsm", "dr5rgyx", "dr5rggr", "dr5rgg6", "dr5ruef",
// "dr5runn", "dr5ruqf", "dr5rgyr", "dr5rut8", "dr5ruq7", "dr5ru5z", "dr5rum5", "dr5runm", "dr5ru2z", 
// "dr5rgu9", "dr5rgyz", "dr5rgbj", "dr5rgbs", "dr5rgu3", "dr5runb", "dr5rux9", "dr5rudx", "dr5rurr",
// "dr5rgv5", "dr5ru2h", "dr5rutn", "dr5rucc", "dr5rgfu", "dr5rgdn", "dr5rge5", "dr5rukt", "dr5rgve", 
// "dr5rgyf", "dr5rump", "dr5ru4r", "dr5run4", "dr5ru29", "dr5rguf", "dr5rgcj", "dr5rutu", "dr5ruhb",
// "dr5rukw", "dr5ruy8", "dr5ruqy", "dr5runx", "dr5ruwu", "dr5rge7", "dr5ruc3", "dr5rusn", "dr5rud5", 
// "dr5rum1", "dr5rsrv", "dr5rgbf", "dr5ruk3", "dr5rutf", "dr5ru3p", "dr5ruw1", "dr5ru1c", "dr5rutx",
// "dr5run2", "dr5rgcy", "dr5ruj6", "dr5runr", "dr5ru9n", "dr5rgvb", "dr5rg9v", "dr5ru4q", "dr5ruh1", 
// "dr5rusb", "dr5rgtz", "dr5rupp", "dr5ruuj", "dr5ruwe", "dr5ruqw", "dr5rgyk", "dr5ruk6", "dr5ru1b",
// "dr5ru4v", "dr5rgwr", "dr5rggw", "dr5rgfw", "dr5run0", "dr5ruqu", "dr5rujm", "dr5runp", "dr5ru4s", 
// "dr5ru0e", "dr5rugf", "dr5ru71", "dr5ru33", "dr5ru1y", "dr5ruj7", "dr5ru35", "dr5rudr", "dr5ru6r",
// "dr5rgyj", "dr5run6", "dr5ru64", "dr5ruq4", "dr5ru4y", "dr5rgg4", "dr5ru3k", "dr5ru19", "dr5ruqs", 
// "dr5rgu5", "dr5rger", "dr5ru79", "dr5rujn", "dr5rudc", "dr5ru8m", "dr5ru3f", "dr5ru9e", "dr5rux4",
// "dr5ruc0", "dr5ruq8", "dr5rurw", "dr5rgwp", "dr5rg9w", "dr5rgye", "dr5rup7", "dr5ruwv", "dr5rusx", 
// "dr5rgfc", "dr5ru3d", "dr5ruuw", "dr5ru2n", "dr5ruk4", "dr5rspg", "dr5ru0z", "dr5rue3", "dr5ruje",
// "dr5rgu1", "dr5ruhq", "dr5ru94", "dr5ruev", "dr5rud8", "dr5ru6u", "dr5rgty", "dr5rgfp", "dr5rutj", 
// "dr5rutb", "dr5ru1e", "dr5ru7v", "dr5rgtr", "dr5rgzj", "dr5rsrc", "dr5ruhs", "dr5rux7", "dr5ruc2",
// "dr5rupz", "dr5ruee", "dr5rujy", "dr5rgvj", "dr5rggt", "dr5ru48", "dr5rusq", "dr5rufj", "dr5rumj", 
// "dr5rume", "dr5ru00", "dr5ru2u", "dr5ru7m", "dr5rgsk", "dr5rgdw", "dr5rum8", "dr5rgfh", "dr5rgvf",
// "dr5ru4d", "dr5rspf", "dr5ru89", "dr5ru5m", "dr5runj", "dr5rgdx", "dr5rusg", "dr5ru36", "dr5rutk", 
// "dr5russ", "dr5ru18", "dr5ru7k", "dr5ru8w", "dr5ru8b", "dr5ruvh", "dr5ru5w", "dr5rgv1", "dr5rgyt",
// "dr5ruh6", "dr5rufd", "dr5rgv3", "dr5ru2g", "dr5rue8", "dr5rgsy", "dr5ru8u", "dr5ru7h", "dr5rug8", 
// "dr5ru3g", "dr5ruw9", "dr5ru32", "dr5rgsx", "dr5rujp", "dr5ruut", "dr5ruv9", "dr5rutt", "dr5rucu",
// "dr5ru1w", "dr5rgeh", "dr5ruk0", "dr5rumn", "dr5rupm", "dr5rugc", "dr5rgbt", "dr5rus2", "dr5ru6j", 
// "dr5ru7f", "dr5ru20", "dr5ruum", "dr5rgv2", "dr5ruj8", "dr5rguh", "dr5ruj2", "dr5ru98", "dr5ru41",
// "dr5rgds", "dr5ruky", "dr5rurh", "dr5ru3z", "dr5ru0b", "dr5rup9", "dr5rgbn", "dr5ruvf", "dr5ru7x", 
// "dr5rujr", "dr5rspv", "dr5rgbq", "dr5run1", "dr5rgcr", "dr5rggk", "dr5ruve", "dr5ru9c", "dr5rgfg",
// "dr5rgc9", "dr5ru9z", "dr5rgvw", "dr5rujs", "dr5rury", "dr5rggb", "dr5rgdt", "dr5rut9", "dr5rux3", 
// "dr5ru5y", "dr5rupk", "dr5ru5e", "dr5rgcm", "dr5ruy4", "dr5ruxj", "dr5rgbp", "dr5ru37", "dr5rugd",
// "dr5ruf3", "dr5rufb", "dr5rgdy", "dr5rux6", "dr5ru6e", "dr5rgc1", "dr5ru5v", "dr5rune", "dr5ruv3", 
// "dr5rut1", "dr5rus8", "dr5rutz", "dr5ru6f", "dr5rgen", "dr5ru30", "dr5ru5n", "dr5rg9y", "dr5rucf",
// "dr5ruku", "dr5rud1", "dr5rud0", "dr5rus4", "dr5rguk", "dr5rufm", "dr5ru1s", "dr5ru0p", "dr5rues", 
// "dr5rut7", "dr5rgvc", "dr5ruke", "dr5rgbg", "dr5ruws", "dr5run9", "dr5ru5h", "dr5ruf7", "dr5rgbr",
// "dr5rup4", "dr5rggv", "dr5runf", "dr5rukn", "dr5ru5x", "dr5ru9y", "dr5rgfk", "dr5rgut", "dr5rgfd", 
// "dr5rggh", "dr5runc", "dr5ru2c", "dr5ruds", "dr5rgcf", "dr5rurj", "dr5ru6v", "dr5rgc0", "dr5rugy",
// "dr5ru6x", "dr5rspw", "dr5rgup", "dr5ru5t", "dr5ru17", "dr5rggc", "dr5rumz", "dr5rgsj", "dr5ru4w", 
// "dr5ru57", "dr5rujv", "dr5ru13", "dr5ruq5", "dr5rumv", "dr5rspx", "dr5ruhj", "dr5ru8y", "dr5ru6m",
// "dr5ruqd", "dr5rurp", "dr5rgv7", "dr5ru9d", "dr5ruu9", "dr5rur0", "dr5rgwn", "dr5ruep", "dr5rsrz", 
// "dr5ru25", "dr5ru5d", "dr5ru93", "dr5rgu6", "dr5rgyd", "dr5rsr8", "dr5ruey", "dr5ru73", "dr5ruhd",
// "dr5rgy0", "dr5rukj", "dr5ru7d", "dr5rusk", "dr5ruu7", "dr5rget", "dr5rspu", "dr5run8", "dr5ruem", 
// "dr5ru02", "dr5rgfe", "dr5ruk2", "dr5ru8e", "dr5ru46", "dr5runq", "dr5ruj1", "dr5rgvk", "dr5ru6n",
// "dr5ru6y", "dr5ruhx", "dr5ru96", "dr5ruvm", "dr5rgvx", "dr5rgyy", "dr5ru68", "dr5rgus", "dr5rgu4", 
// "dr5rgy5", "dr5rups", "dr5ruw5", "dr5ruvd", "dr5rgtj", "dr5rguv", "dr5ruf1", "dr5rgbv", "dr5ru16",
// "dr5ru0v", "dr5ru0j", "dr5rund", "dr5ru6c", "dr5ru4z", "dr5rugu", "dr5ru5r", "dr5ru2s", "dr5rgf5", 
// "dr5rgv6", "dr5ruhf", "dr5rgvu", "dr5ru7n", "dr5ruqc", "dr5ru7w", "dr5ruhv", "dr5rukb", "dr5rugb",
// "dr5ru2d", "dr5ru27", "dr5ru8d", "dr5rguz", "dr5rurm", "dr5ruud", "dr5ru7b", "dr5ru10", "dr5rust", 
// "dr5rurq", "dr5rukx", "dr5rut0", "dr5rgez", "dr5ruqn", "dr5rgg0", "dr5ruv4", "dr5ru42", "dr5ru05",
// "dr5ruft", "dr5ru2w", "dr5rgzp", "dr5rutp", "dr5rums", "dr5rumx", "dr5rur8", "dr5ruy3", "dr5ru63", 
// "dr5rus6", "dr5ru45", "dr5ru0g", "dr5ruh3", "dr5ru5c", "dr5ru34", "dr5runv", "dr5ruxh", "dr5ru4g",
// "dr5rupj", "dr5ruez", "dr5rupu", "dr5ru1f", "dr5ruf4", "dr5ru2k", "dr5ruhz", "dr5rspy", "dr5ruub", 
// "dr5ruwc", "dr5rute", "dr5ru2e", "dr5rgff", "dr5rufk", "dr5ru0u", "dr5ru06", "dr5rgy7", "dr5ruww",
// "dr5rur3", "dr5rsps", "dr5ru3n", "dr5rgub", "dr5runy", "dr5ruf5", "dr5rucd", "dr5rgeg", "dr5rgv9", 
// "dr5rggy", "dr5rumt", "dr5rgce", "dr5rguu", "dr5rgtk", "dr5rusj", "dr5rud6", "dr5rsxb", "dr5ru4m",
// "dr5ruf6", "dr5ru2v", "dr5ru6g", "dr5ruq1", "dr5ruw0", "dr5rgs5", "dr5rug0", "dr5ru4c", "dr5rufs", 
// "dr5ruqk", "dr5rum7", "dr5ru39", "dr5ru0h", "dr5rutq", "dr5ru6t", "dr5rgcd", "dr5ru9x", "dr5rsry",
// "dr5ruh2", "dr5ruer", "dr5rgfr", "dr5rsxc", "dr5rgdp", "dr5rupt", "dr5rujz", "dr5rspt", "dr5rgsw", 
// "dr5rueb", "dr5rug1", "dr5ru5b", "dr5ruc6", "dr5rguy", "dr5rgdh", "dr5ru76", "dr5runh", "dr5rum9",
// "dr5rukv", "dr5rujd", "dr5ruuc", "dr5ru9u", "dr5ru2j", "dr5rgy6", "dr5rutm", "dr5ru3m", "dr5ru8f", 
// "dr5rgsq", "dr5ru7r", "dr5ruqr", "dr5ruty", "dr5ru7e", "dr5ru3v", "dr5rgyp", "dr5rgf4", "dr5ruy9",
// "dr5ruju", "dr5rukz", "dr5rumg", "dr5ruqv", "dr5rup2", "dr5rup3", "dr5rgyg", "dr5ruy5", "dr5ruhk", 
// "dr5rgvm", "dr5ru8q", "dr5ruq9", "dr5rueu", "dr5rudq", "dr5rgf2", "dr5rgfj", "dr5ru3y", "dr5ruwm",
// "dr5rudg", "dr5rggs", "dr5ruv2", "dr5rumr", "dr5rufg", "dr5rgb7", "dr5ru8z", "dr5ruwf", "dr5ru74", 
// "dr5rguj", "dr5rukh", "dr5rudu", "dr5rgc7", "dr5rgdu", "dr5ruu1", "dr5rgfx", "dr5ruw3", "dr5rgfz",
// "dr5rupd", "dr5rurn", "dr5rgcw", "dr5ruvs", "dr5rgcp", "dr5ru9r", "dr5ru5k", "dr5ruc1", "dr5ru99", 
// "dr5ruw2", "dr5rugg", "dr5rggq", "dr5rges", "dr5ruc8", "dr5rum0", "dr5runu", "dr5rudf", "dr5rusy",
// "dr5ru0s", "dr5rgcz", "dr5rgej", "dr5rujj", "dr5ruce", "dr5rgv0", "dr5ru95", "dr5ruh9", "dr5ru0n", 
// "dr5rsrb", "dr5ru7t", "dr5rgv8", "dr5ruwy", "dr5ru5q", "dr5rgss", "dr5ru9h", "dr5ru8t", "dr5rgum",
// "dr5ruhr", "dr5ru54", "dr5rupq", "dr5rgee", "dr5ru0d", "dr5rugm", "dr5rgfm", "dr5runz", "dr5rurs", 
// "dr5rgew", "dr5rgfv", "dr5ru61", "dr5rurd", "dr5ru2f", "dr5rukf", "dr5ru1t", "dr5ruw7", "dr5rupw",
// "dr5ru7s", "dr5rusd", "dr5rgyn", "dr5rumu", "dr5ruw6", "dr5rurk", "dr5ru4k", "dr5ruff", "dr5ruqq", 
// "dr5ruu5", "dr5ru44", "dr5rgbc", "dr5rgtq", "dr5rur6", "dr5rgg7", "dr5rugn", "dr5ru2q", "dr5ruqt",
// "dr5rue2", "dr5ruvg", "dr5rguw", "dr5rup5", "dr5ru67", "dr5rgeq", "dr5rgyv", "dr5rgzr", "dr5ruj5", 
// "dr5ru75", "dr5run5", "dr5rue5", "dr5ru77", "dr5rucg", "dr5ru0q", "dr5rupx", "dr5rurb", "dr5ruy2",
// "dr5ruqh", "dr5ruv0", "dr5rug6", "dr5ru3s", "dr5rgvv", "dr5ruhc", "dr5ru9w", "dr5rgf9", "dr5rgsu", 
// "dr5ru5g", "dr5ru7g", "dr5rue9", "dr5ruph", "dr5rggu", "dr5ru38", "dr5ru0m", "dr5rgtt", "dr5rgue",
// "dr5rusw", "dr5rujh", "dr5ru9p", "dr5rgtp", "dr5rgeu", "dr5ru6k", "dr5rugv", "dr5ruq3", "dr5ruq2", 
// "dr5run7", "dr5rsrt", "dr5rufe", "dr5rgby", "dr5rush", "dr5rgey", "dr5rgvn", "dr5ru66", "dr5ru9g",
// "dr5rg9p", "dr5rufc", "dr5rukq", "dr5ruqp", "dr5rumb", "dr5rgex", "dr5ruew", "dr5ru5p", "dr5ruts", 
// "dr5rspb", "dr5rgvh", "dr5rgu2", "dr5rutg", "dr5rufv", "dr5ruuf", "dr5rumc", "dr5ru6q", "dr5ruvj",
// "dr5ru87", "dr5rudv", "dr5rgy3", "dr5rgyq", "dr5rug9", "dr5ruq0", "dr5ruxn", "dr5ruk8", "dr5ru43", 
// "dr5ru4j", "dr5rgfn", "dr5ru3b", "dr5rgbx", "dr5rut6", "dr5ruf0", "dr5rug5", "dr5rujx", "dr5ru92",
// "dr5ru3r", "dr5rgyu", "dr5rspc", "dr5rgzh", "dr5ru4h", "dr5ru0c", "dr5ru28", "dr5ru62", "dr5rgfy", 
// "dr5ru51", "dr5rugt", "dr5ruru", "dr5rgug", "dr5ru8g", "dr5rgux", "dr5ru2t", "dr5rukm", "dr5ruj9",
// "dr5ru9v", "dr5ru7q", "dr5rgc2", "dr5ru0k", "dr5rgf3", "dr5rukg", "dr5ruwb", "dr5rukp", "dr5rusp", 
// "dr5rukr", "dr5rgur", "dr5rgc6", "dr5rgg2", "dr5rurv", "dr5rgev", "dr5ru7j", "dr5rumf", "dr5rsr9",
// "dr5ruun", "dr5rus0", "dr5rued", "dr5ruht", "dr5ruvb", "dr5rum2", "dr5ruqm", "dr5rure", "dr5ru6h", 
// "dr5rsre", "dr5rudh", "dr5ru11", "dr5rut5", "dr5rgyh", "dr5ru3h", "dr5ru4t", "dr5rux0", "dr5rgtn",
// "dr5rgs7", "dr5ruuk", "dr5ru4f", "dr5rus3", "dr5rugj", "dr5rug3", "dr5rgvy", "dr5rudw", "dr5rgg5", 
// "dr5rugw", "dr5rgvq", "dr5ru86", "dr5ruqz", "dr5rue4", "dr5ru09", "dr5rurg", "dr5rgcg", "dr5rugs",
// "dr5ru2x", "dr5ru3c", "dr5rumm", "dr5rgsv", "dr5rggf", "dr5ruwq", "dr5rggd", "dr5ruc4", "dr5ru55", 
// "dr5rgcq", "dr5ru4e", "dr5rgcx", "dr5ru0x", "dr5ruse", "dr5rumw", "dr5rgvp", "dr5rgvg", "dr5ruh0",
// "dr5rgu7", "dr5rgtw", "dr5ruk1", "dr5ru5f", "dr5ruks", "dr5ruu8", "dr5ru8c", "dr5ru8x", "dr5rgsr", 
// "dr5rgy1", "dr5rgc8", "dr5ru3j", "dr5rudn", "dr5rgym", "dr5ru4b", "dr5rsrd", "dr5rgst", "dr5rudd",
// "dr5rgud", "dr5rurx", "dr5rgzn", "dr5ru0y", "dr5rgdk", "dr5rumd", "dr5ruhm", "dr5rup6", "dr5ru3w", 
// "dr5ruen", "dr5rudy", "dr5rudb", "dr5rumy", "dr5rsru", "dr5rgv4", "dr5rgy2", "dr5ru2b", "dr5rukc",
// "dr5ru9q", "dr5rusr", "dr5rgvs", "dr5rgcb", "dr5ruhy", "dr5ru49", "dr5ru4x", "dr5rugh", "dr5rgcn", 
// "dr5ruwd", "dr5rus7", "dr5rusc", "dr5ru8s", "dr5ru9f", "dr5rgsn", "dr5ru1v", "dr5ruqe", "dr5rujw",
// "dr5ruu4", "dr5rgc3", "dr5rgem", "dr5rgtm", "dr5ru84", "dr5ruc9", "dr5rgdq", "dr5ru07", "dr5rupe", 
// "dr5rgbu", "dr5ruwt", "dr5rgyw", "dr5rux5", "dr5rgse", "dr5ru23", "dr5rue6", "dr5ruq6", "dr5ru9t", 
// "dr5rup8", "dr5ruh5", "dr5rudm", "dr5ru52", "dr5rgcu", "dr5rgvt", "dr5ruug", "dr5ru1u", "dr5rgek", 
// "dr5rukd", "dr5rgu8", "dr5rud3", "dr5rgck", "dr5ruh7", "dr5rgdr", "dr5ruuq", "dr5ru7u", "dr5rgvz", 
// "dr5ru56", "dr5rurf", "dr5ru80", "dr5ru8k", "dr5rujb", "dr5rg9r", "dr5ru8v", "dr5rubb", "dr5ru0w", 
// "dr5rsrg", "dr5rueg", "dr5ru2r", "dr5rgvr", "dr5rggg", "dr5rutr", "dr5rueq", "dr5rgy9", "dr5ruf9", 
// "dr5rurc", "dr5rgtx", "dr5rufu", "dr5rgcs", "dr5ruhh", "dr5rgbw", "dr5rgfq", "dr5ru26", "dr5rgdm", 
// "dr5ruue", "dr5rux8", "dr5ru6s", "dr5ru1x", "dr5rup0", "dr5rueh", "dr5rutd", "dr5ruw8", "dr5ruf2", 
// "dr5rung", "dr5ruh4", "dr5rut4", "dr5ru1q", "dr5ru1m", "dr5ruwk", "dr5rujt", "dr5ru83", "dr5rue0", 
// "dr5ru8h", "dr5ruvk", "dr5rgfb", "dr5ru69", "dr5ru5s", "dr5ruwp", "dr5ru40", "dr5ru88", "dr5ru1k", 
// "dr5ruxk", "dr5rgsp", "dr5rgf1", "dr5rgf7", "dr5ru81", "dr5rudt", "dr5ru1j", "dr5ru1z", "dr5ruqg", 
// "dr5ruth", "dr5rupy", "dr5ru0t", "dr5rusm", "dr5ruuv", "dr5rgsh", "dr5rgf6", "dr5ruhp", "dr5ru1g", 
// "dr5ru0r", "dr5rujk", "dr5rud9", "dr5ru47", "dr5ru4u", "dr5ru6z", "dr5ruu3", "dr5rggn", "dr5rggm", 
// "dr5rgbe", "dr5ruy0", "dr5rumq", "dr5ruv7", "dr5rgg9", "dr5rujf", "dr5ru6b", "dr5rue7", "dr5rgg3", 
// "dr5rude", "dr5ru24", "dr5rud7", "dr5ru14", "dr5rgzq", "dr5ru9s", "dr5rgsz", "dr5ruv8", "dr5rud2", 
// "dr5ru4p", "dr5rudz", "dr5ruk9", "dr5rufh", "dr5ruj3", "dr5rum3", "dr5ruhu", "dr5rggx", "dr5ru9m", 
// "dr5ruge", "dr5ru3e", "dr5rgf0", "dr5rux1", "dr5rusf"

7-bounding

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment