Reverse-engineering Geoguessr
Try it here!
>>> One of my first quality projects back in 2023, I was always into Geoguessr until GoogleAPI increased
the
price for some Streetview services, prmpting Geoguessr to become more expensive.
The problem with re-creating it lies in the idea that there really isnt a way to pull the data on WHERE
Streetview has their panoramas. My first idea was to try and use OverpassAPI to detect where roads are.
This wasnt optimal as there are plenty of countries with data on OverpassAPI yet no panoramas on Google
StreetView.
>>> Thus, to only isolate the panoramas availiable on StreetView, I found the world map provided by
Google of the total coverage for the StreetView:

>>> I got the clear idea to run a script over each pixel of the map, read its colour and if its not white, convert the pixel coordinates to latitude/longitude coordinates.
from PIL import Image
import math
im = Image.open('googlemapscoverage.png')
pix = im.load()
print(im.size)
import numpy as np
import folium
def mercator_to_latlon(x, y, width, height, lat_min=-79.689, lat_max=80.0):
lon = (x / width) * 360.0 - 180.0
y_norm = y / height
lat_rad_max = math.radians(lat_max)
lat_rad_min = math.radians(lat_min)
merc_max = math.log(math.tan(math.pi / 4 + lat_rad_max / 2))
merc_min = math.log(math.tan(math.pi / 4 + lat_rad_min / 2))
merc_y = merc_min + (merc_max - merc_min) * (1 - y_norm)
lat = math.degrees(2 * math.atan(math.exp(merc_y)) - math.pi / 2)
return lat, lon-0.5
i=0
f = open("coordinates.txt", "a")
coordinates = []
for x in range(im.size[0]):
i+=1
for y in range(im.size[1]):
if(pix[x,y][2]>pix[x,y][0] and pix[x,y][0]<120):
f.write(str(mercator_to_latlon(x,y,im.size[0], im.size[1])[0])+','+str(mercator_to_latlon(x,y,im.size[0], im.size[1])[1])+'\n')
>>> Yes, storing the coordinates in a txt file. Not standard, but very fast to look up when loaded into RAM later. Now you may notice that -79.689, and not a -80 like normal coordinate planes. That is due to the provided Mercator projection image NOT being a proper Mercator projection, but a bit croppet to exclude some of the poles. This error propagated strongly, leading to an issue of offset coordinates when mapping them with -80 instead.

>>> The number of -79.689 was drived by binary searching the most precise value: when -79 was an
overcorrection,
-79.5 was tried, and so on, after which I eyeballed some more precision, as a perfect value is hard to
arrive to.
>>> When I was satisfied with the results of the coordinate list, another problem remained. The coordinate
mesh
that I created was an approximation: the entire city of Sapporo was represented by 5 coordinate points.
Thus I still needed to use OverpassApi, but in a much smaller capacity, leading to a lower amount of queries
made to the free API Server.
var s = 0.025;//size of square to scan
const query = `
[bbox:${lat},${long},${lat - s},${long - s}]
[out:json]
[timeout:1000]
;
(
way
[highway~"^(primary|secondary|tertiary|residential|unclassified)$"]
(
${lat - s},
${long - s},
${lat + s},
${long + s}
);
way
[amenity~"^(parking|fuel|school|hospital|police|fire_station)$"]
(
${lat - s},
${long - s},
${lat + s},
${long + s}
);
way
[shop]
(
${lat - s},
${long - s},
${lat + s},
${long + s}
);
);
out geom;
`
result = await fetch('https://overpass-api.de/api/interpreter', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'data=' + encodeURIComponent(query)
})
.then((data) => data.json());
>>> And now I attach the free part of the StreetView API provided by Google:
async function checkStreetViewAvailability(lat, long) {
const radius = 10; // meters to check around the point
const response = await fetch(
`https://maps.googleapis.com/maps/api/streetview/metadata?location=${lat},${long}&radius=${radius}&key=APIKEY`
);
const data = await response.json();
return data.status === 'OK';
}
const locations = await shuffleArray(result['elements']);
for (let i = 0; i < locations.length; i++) {
const b = await checkStreetViewAvailability(locations[i]['bounds']['minlat'], locations[i]['bounds']['minlon'])
if (b == true) {
success = true;
resolve([locations[i]['bounds']['minlat'], locations[i]['bounds']['minlon']]);
break;
}
}
if (!success) {
//restart the search
}
>>> This is a recursive approach that loops through all possible locations in the scanned square, and moves on to a new one if no locations are found. Usually this doesnt take too long, and the loading barely takes more than 5 seconds in most cases.
>>> This has been a quick fun creative project! Thanks for reading.