Geohash1 is een techniek waarmee geografische locaties kunnen gecodeerd worden als een korte reeks letters en cijfers. Zo duidt geohash ezs42 bijvoorbeeld een gebied aan rond de coördinaat (-5.6, 42.6). Geohashes hebben de eigenschap dat je hun nauwkeurigheid kunt variëren en dat je op het einde tekens kunt verwijderen om de code korter te maken (waardoor er geleidelijk ook nauwkeurigheid verloren gaat). Door het gradueel inboeten aan nauwkeurigheid zullen locaties die bij elkaar in de buurt liggen vaak (maar niet altijd) met dezelfde tekens beginnen. Hoe meer gemeenschappelijk tekens vooraan, hoe dichter twee locaties bij elkaar in de buurt liggen.

Stel bijvoorbeeld dat we een gebied rond coördinaat (-5.6, 42.6) — in onderstaande kaart aangeduid met een rode markering — willen voorstellen door een geohash. Daarvoor verdelen we eerst de wereldkaart met een verticale lijn in twee helften die we elk aanduiden met een binaire waarde: 0 (linkerhelft) of 1 (rechterhelft). Omdat de locatie in de linkerhelft ligt, is het eerste bit van de geohash gelijk aan 0.

geohash
De locatie ligt in de linkerhelft van de wereldkaart (westelijk halfrond, ten westen van de nulmeridiaan), waardoor het eerste bit gelijk is aan 0.

Om de locatie nauwkeuriger aan te duiden, delen we de helft waarin de locatie ligt verder op in twee helften. Nu doen we de verdeling echter met een horizontale lijn en duiden we elke helft opnieuw aan met een binaire waarde: 0 (onderste helft) en 1 (bovenste helft). Omdat de locatie in de bovenste helft ligt, is het tweede bit van de geohash gelijk aan 1.

geohash
De locatie ligt in de bovenste helft van de wereldkaart (noordelijk halfrond, boven de evenaar), waardoor het tweede bit gelijk is aan 1.

Daarna delen we de helft waarin de locatie ligt verder op in twee helften, maar dan nu terug met een verticale lijn. Omdat de locatie in de rechterhelft ligt, is het derde bit van de geohash gelijk aan 1.

geohash
De locatie ligt in de rechterhelft van het resterende gebied, waardoor het derde bit gelijk is aan 1.

Door het resterende gebied telkens in twee te verdelen — afwisselend met een verticale en een horizontale lijn — verdubbelen we in elke stap de nauwkeurigheid van de geohash, maar wordt de geohash ook telkens één bit langer.

Opgave

Zet een gegeven geohash om naar coördinaat $$(\lambda, \varphi)$$ met $$\lambda, \varphi \in \mathbb{R}$$. Hierbij geldt dat lengtegraad $$\lambda \in [-180, 180]$$ en breedtegraad $$\varphi \in [-90, 90]$$. We illustreren het algoritme dat je hiervoor moet gebruiken aan de hand van geohash ezs42 die we ook in de inleiding gebruikt hebben.

Eerst zetten we de geohash om naar zijn decimale waarde. Een geohash gebruikt 32 verschillende tekens: de cijfers 09 en de letters az, behalve de letters a, i, l en o (er wordt geen onderscheid gemaakt tussen hoofdletters en kleine letters). Daardoor kan de geohash geïnterpreteerd worden als een getal in een talstelsel met grondtal 32 (base-32). Een getal $$\left(c_{n}c_{n-1}\ldots c_{2}c_{1}c_{0}\right)_b$$ in een talstelsel met grondtal $$b \in \mathbb{N}_0$$ kan als volgt omgezet worden naar zijn decimale waarde: \[ \sum_{i=0}^{n} \left(c_i\right)_{10} \times b^i \] waarbij de notatie $$\left(c_i\right)_{10}$$ gebruikt wordt voor de decimale waarde van het cijfer $$c_i$$ uit het talstelsel met grondtal $$b$$. De tekens $$\texttt{t}$$ van een geohash krijgen de volgende decimale waarde:

$$\texttt{t}$$ 0 1 2 3 4 5 6 7 8 9 b c d e f g
$$\texttt{t}_{10}$$ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
 
$$\texttt{t}$$ h j k m n p q r s t u v w x y z
$$\texttt{t}_{10}$$ 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

Zo wordt de decimale waarde van geohash ezs42 met $$b = 32$$ berekend als \[ \left(2 \times 32^{0}\right) + \left(4 \times 32^{1}\right) + \left(24 \times 32^{2}\right) + \left(31 \times 32^{3}\right) + \left(13 \times 32^{4}\right) = 14672002 \] Deze decimale waarde zetten we vervolgens om naar een bitstring met de binaire waarde van de geohash. In Python kan je de ingebouwde functie bin gebruiken om een decimale waarde om te zetten naar zijn binaire waarde.

>>> bin(14672002)
'0b110111111110000010000010'

Bij de omzetting van de decimale waarde naar de bitstring moeten we eventueel voorloopnullen toevoegen aan de binaire waarde, omdat elk teken van een geohash 5 bits codeert ($$2^5 = 32$$). Voor geohash ezs42 krijgen we dan de bitstring 0110111111110000010000010 met 25 bits (één voorloopnul).

Als we nu de bits van de bitstring van links naar rechts nummeren vanaf 0, dan coderen de even bits de lengtegraad (0111110000000) en de oneven bits de breedtegraad (101111001001). Elk van deze bitstrings kan dus gebruikt worden als instructie voor een reeks opeenvolgende opdelingen, waarbij de bits één per één worden afgelopen van links naar rechts. Voor de breedtegraad wordt het interval [-90, 90] in twee gelijke delen verdeeld, waardoor we twee intervallen krijgen: [-90, 0] en [0, 90]. Omdat het eerste bit 1 is, wordt het hogere interval gekozen als het nieuwe interval. Deze procedure wordt herhaald voor alle bits in de bitstring. De uiteindelijk breedtegraad ligt in het midden van het interval dat finaal bekomen wordt. Voor de lengtegraad gebruiken we exact dezelfde procedure, maar vertrekken we van het interval [-180, 180].

Zo is het eerste bit in de bitstring voor de breedtegraad (101111001001) gelijk aan 1, waardoor we weten dat de breedtegraad ergens tussen 0° en 90° ligt. Zonder bijkomende bits zouden we schatten dat de breedtegraad 45° was, wat ons een fout van ±45° oplevert. Omdat er nog meer bits zijn, kunnen we doorgaan met het volgende bit, waarbij elke volgende bit de fout halveert. Onderstaande tabel toont het effect van elke bit. In elke stap wordt de gekozen helft van het interval groen gemarkeerd: bit 0 kiest de onderste helft en bit 1 kiest de bovenste helft. De kolom "breedtegraad" toont de schatting van de breedtegraad als middelpunt van het gekozen interval. Elke volgende bit maakt deze schatting nauwkeuriger.

positie bit ondergrens midden bovengrens breedtegraad maximale fout
0 1 -90.000 0.000 90.000 45.000 45.000
1 0 0.000 45.000 90.000 22.500 22.500
2 1 0.000 22.500 45.000 33.750 11.250
3 1 22.500 33.750 45.000 39.375 5.625
4 1 33.750 39.375 45.000 42.188 2.813
5 1 39.375 42.188 45.000 43.594 1.406
6 0 42.188 43.594 45.000 42.891 0.703
7 0 42.188 42.891 43.594 42.539 0.352
8 1 42.188 42.539 42.891 42.715 0.176
9 0 42.539 42.715 42.891 42.627 0.088
10 0 42.539 42.627 42.715 42.583 0.044
11 1 42.539 42.583 42.627 42.605 0.022

Op dezelfde manier kunnen we ook een schatting maken van de lengtegraad (bitstring 0111110000000).

positie bit ondergrens midden bovengrens lengtegraad maximale fout
0 0 -180.000 0.000 180.000 -90.000 90.000
1 1 -180.000 -90.000 0.000 -45.000 45.000
2 1 -90.000 -45.000 0.000 -22.500 22.500
3 1 -45.000 -22.500 0.000 -11.250 11.250
4 1 -22.5000 -11.250 0.000 -5.625 5.625
5 1 -11.250 -5.625 0.000 -2.813 2.813
6 0 -5.625 -2.813 0.00 -4.219 1.406
7 0 -5.625 -4.219 -2.813 -4.922 0.703
8 0 -5.625 -4.922 -4.219 -5.273 0.352
9 0 -5.625 -5.273 -4.922 -5.449 0.176
10 0 -5.625 -5.449 -5.273 -5.537 0.088
11 0 -5.625 -5.537 -5.449 -5.581 0.044
12 0 -5.625 -5.581 -5.537 -5.603 0.022

Gevraagd wordt:

Voorbeeld

>>> geo2dec('ezs42')
14672002
>>> geo2dec('DRUGGED')
13684424108
>>> geo2dec('ZUR1CH')
1068205424

>>> geo2bin('ezs42')
'0110111111110000010000010'
>>> geo2bin('DRUGGED')
'01100101111101001111011110110101100'
>>> geo2bin('ZUR1CH')
'111111101010111000010101110000'

>>> unravel('0110111111110000010000010')
('0111110000000', '101111001001')
>>> unravel('01100101111101001111011110110101100')
('010011001101110010', '10111110111101110')
>>> unravel('111111101010111000010101110000')
('111111110000100', '111000100111100')

>>> bin2coord('0111110000000', -180, 180)
(-5.625, -5.5810546875)
>>> bin2coord('101111001001', -90, 90)
(42.5830078125, 42.626953125)
>>> bin2coord('010011001101110010', -180, 180)
(-71.91375732421875, -71.91238403320312)
>>> bin2coord('10111110111101110', -90, 90)
(44.27215576171875, 44.273529052734375)
>>> bin2coord('111111110000100', -180, 180)
(178.6376953125, 178.648681640625)
>>> bin2coord('111000100111100', -90, 90)
(69.23583984375, 69.2413330078125)

>>> geo2coord('ezs42')
(-5.60302734375, 42.60498046875)
>>> geo2coord('DRUGGED')
(-71.91307067871094, 44.27284240722656)
>>> geo2coord('ZUR1CH')
(178.6431884765625, 69.23858642578125)