What this covers
Placing an image on the earth and in time without metadata. The place comes from matching what is in the frame against the world; the time comes from the sun, which is the one thing in the picture whose behaviour is exactly calculable. A hard geolocation is hours of work, and the chronolocation that follows it is arithmetic you can show someone.
Method
- Inventory the frame. List everything identifiable: road markings, utility pole design, plug sockets, licence-plate format, language and script on signage, vegetation, kerb style, architecture, mountain profile.
- Narrow the region. Those details constrain the country or region long before they give you a point. Pole and bollard design alone often gets you to a handful of countries.
- Find a searchable anchor. A business name, a phone number on a van, a street name, a bus route number. One readable sign collapses the search; spend your effort on making one legible before you spend it on scanning imagery.
- Match terrain. Ridgelines are fingerprints and are visible from tens of kilometres away. This is the step that works when there is no signage at all, and the step that fails on flat ground.
- Confirm with imagery. Satellite and street view from the era of the photo. Match building footprints and the relative geometry of fixed objects, not the general vibe of the scene.
- Measure the shadow, then compute. Direction gives you the sun’s azimuth, the length-to-height ratio gives you its elevation. Together they give a time of day and, usually, two candidate date windows per year rather than one.
- Say what would falsify it. A geolocation you cannot break is one nobody checked. Name the feature that would have to be in the wrong place for your answer to be wrong.
The judgement calls: decide early whether you are doing signage work or terrain work, because they need different tools and mixing them wastes hours. Decide whether the date is given or derived — if you take the date from the caption and then use the sun to “confirm” the caption, you have proved nothing. And decide whether a precise coordinate should be published at all, before you find one.
The shadow arithmetic
A vertical object of height h casting a shadow of length s on level ground fixes the sun’s
elevation angle:
tan(elevation) = h / s
h = 5.00 m, s = 8.20 m
elevation = atan(5.00 / 8.20) = atan(0.60976) = 31.37 degrees
check: 5.00 / tan(31.37 deg) = 5.00 / 0.60976 = 8.20 m
The shadow points directly away from the sun, so the sun’s compass bearing is the shadow’s bearing plus 180:
shadow bearing measured off the image : 062 deg
sun azimuth : (062 + 180) mod 360 = 242 deg
Three things break this. The ground must be level — a shadow running downhill is longer than the
arithmetic expects and will push your elevation too low. The object must be vertical and you must
be measuring its true height, not its foreshortened height in the frame. And elevation here is
geometric; at elevations below about 5 degrees atmospheric refraction lifts the apparent sun by
roughly half a degree, which matters at sunrise and sunset and nowhere else.
Then the part people get wrong: an azimuth and elevation pair does not identify one date. The sun retraces its declination on the way out of winter and on the way back in, so almost every pair matches two windows in the year, roughly symmetric about the solstice. Expect two answers and rule one out with something other than the sun — foliage, snow, an event in the background, the clothes people are wearing.
Key tools
SunCalc
Web only, free, no account. Give it a coordinate and a date and it draws the sun’s track for that day with the numbers underneath: altitude, azimuth, and a shadow length readout for an object height you type in. That last field is why it beats the generic ephemeris sites — you can go straight from the measurement you took off the image to the time that produces it, without converting anything.
1. Enter the coordinate in "Set Lat/Lon", or drag the map pin onto the spot.
2. Set month and date in the dropdowns. Note the timezone line (TZ) it reports; everything
downstream is in that zone, and it is the usual source of a one-hour error.
3. Drag the time slider and watch two readouts: "Azimuth" against your measured sun bearing,
and "Shadow length [m]" against the shadow you measured, with "at an object level [m]"
set to the object's real height.
4. When both match at once, you have a time. When only the azimuth matches, you have two
times -- morning and afternoon mirror each other, and length is what separates them.
5. "Reverse Calculation" works the other way: fix the shadow and ask for the time.
6. Capture the page URL. The state lives in the fragment --
#/<lat>,<lon>,<zoom>/<YYYY.MM.DD>/<HH:MM>/<object height> -- so the link reproduces the
exact reading rather than just the tool.
The azimuth it reports is a compass bearing measured clockwise from north, which is what you measured off the image, so no conversion is needed here. Read-offs from a slider are worth about a minute of precision at best; quote a window, not a timestamp. And the whole result is conditional on the coordinate — a 50 m position error barely moves the azimuth, but a wrong date moves it by degrees per week away from the solstices, which is exactly where the two-candidate-window problem bites.
ShadeMap and Shadowmap
Also web only. These solve the opposite problem: when the shadow in your image is cast by a building or a ridge rather than by the subject, you cannot measure an object height, so SunCalc’s arithmetic has nothing to chew on. These render the shadows that real geometry casts at a chosen moment and you match the pattern instead.
ShadeMap (shademap.app)
- loads OpenStreetMap building footprints plus a terrain model, so both urban and
mountain shadows are simulated
- date and time slider at the bottom; the shadow polygons redraw live
- the sun-exposure analytics (hourly / daily / annual) are a solar-study feature,
not an investigative one -- ignore them for this work
- free for interactive use on the site; the paid tiers are for embedding the engine
in your own map, not for using it here
Shadowmap (shadowmap.org)
- 3D buildings with cast shadows, better for dense city blocks
- set the date and time, then orbit the camera to the photographer's approximate
position and compare the shadow edges against the frame
- sign-in gates some of the time controls
What to capture either way: a screenshot at the matched time, the coordinate, the
date, the stated timezone, and the building or ridge you matched on.
Building shadows are only as good as the building heights in OpenStreetMap, which are frequently absent, guessed, or recorded for a structure that has since been extended. Terrain shadows are more trustworthy because the DEM is measured. Neither knows about trees, awnings, scaffolding or a crane that was on site that month — so an unexplained shadow is a reason to doubt your position before it is a reason to doubt the tool.
suncalc (Python)
The same model as the website, as a library, which is what you want once you are checking more than one candidate. Give it a coordinate and a timestamp and it returns the sun’s position; loop it over a year and you get every moment that matches your measurement, which is the honest way to find both date windows instead of stopping at the first one.
pipx install suncalc # or: pip install suncalc pandas
import math, datetime
import pandas as pd
from suncalc import get_position, get_times
lat, lon = 38.7075, -9.1364 # candidate coordinate
height, shadow = 5.00, 8.20 # metres, measured off the image
shadow_bearing = 62.0 # degrees, measured off the image
# the two numbers the image gives you
elevation = math.degrees(math.atan(height / shadow)) # 31.37
sun_bearing = (shadow_bearing + 180) % 360 # 242.0
def sun(dt):
# suncalc returns radians, and its azimuth is measured from SOUTH toward west --
# add 180 degrees to get the compass bearing you measured off the image
p = get_position(pd.Timestamp(dt), lon, lat)
return (math.degrees(p["azimuth"]) + 180) % 360, math.degrees(p["altitude"])
# sweep the whole year at 5-minute resolution: this is what exposes the second window
start = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
for step in range(0, 365 * 288):
dt = start + datetime.timedelta(minutes=5 * step)
az, alt = sun(dt)
if abs(az - sun_bearing) < 1.0 and abs(alt - elevation) < 0.5:
print(f"{dt:%Y-%m-%d %H:%M} UTC az {az:6.1f} elev {alt:5.1f}")
# sanity-check the day itself: if your candidate time is after dusk, the coordinate is wrong
t = get_times(pd.Timestamp("2026-03-22 12:00:00"), lon, lat)
print("sunrise", t["sunrise"], "solar noon", t["solar_noon"], "sunset", t["sunset"])
Two conventions to hold onto, because getting either wrong silently produces a plausible, wrong
answer. Azimuth comes back in radians measured from south, so the + 180 above is not optional.
Altitude is geometric and ignores refraction. Pass timezone-aware UTC datetimes and convert to
local time at the very end, after you have checked whether the location observes DST on that date
— a shadow matched in March against a September local time is the most common way this analysis
goes quietly wrong. The package is thin and has not changed since 2023; that is fine for a model
of celestial mechanics, and it is not a sign of anything.
pysolar
A second implementation, which is the point of using it: run the same coordinate and timestamp through both and you are checking your arithmetic rather than one library’s. It is also the more natural fit when you want irradiance or want to sweep without pandas in the way.
pipx install pysolar # or: pip install pysolar
import datetime
from pysolar.solar import get_altitude, get_azimuth
lat, lon = 38.7075, -9.1364
when = datetime.datetime(2026, 6, 15, 12, 30, tzinfo=datetime.timezone.utc)
# pysolar's azimuth is already a compass bearing, clockwise from north -- no offset needed
print("altitude", get_altitude(lat, lon, when)) # 56.80
print("azimuth ", get_azimuth(lat, lon, when)) # 216.45
# cross-check against suncalc for the same instant: 56.79 / 216.36
# agreement to a tenth of a degree means the inputs are right; a whole-degree gap
# almost always means one of the two got a naive datetime and assumed local time
get_altitude applies a refraction correction by default, which is why it sits a hundredth of a
degree off suncalc’s geometric value near the zenith and further off near the horizon — at 5
degrees elevation the two will differ by about half a degree, and pysolar is the one closer to what
a camera actually saw. Passing a naive datetime is the failure mode: it will be interpreted as
UTC and you will get an answer that looks fine and is hours wrong.
ShadowFinder
Bellingcat’s inversion of the problem, for when you have no candidate coordinate at all. Feed it an object height, a shadow length and a UTC timestamp and it maps every point on earth where a shadow of that ratio is possible at that instant — a band, not a point, but a band you can intersect with everything else you know.
pipx install shadowfinder # or: pip install shadowfinder
# object height, shadow length, date, time -- POSITIONAL, in that order
shadowfinder find 5 8.2 2026-03-22 16:00:00 --time_format=utc
# when you already have the sun's elevation rather than a height/length pair
shadowfinder find_sun 31.37 2026-03-22 16:00:00 --time_format=utc
# the subcommands and their arguments, before you guess
shadowfinder find --help
shadowfinder find_sun --help
# writes a PNG map into the current directory, named for its inputs:
# shadow_finder_20260322-160000-Utc_5_8.2.png
ls shadow_finder_*.png
# the timezone grid it uses for local-time input is generated once and cached
shadowfinder generate_timezone_grid
The older --object-height / --shadow-length / --date-time flag form that circulates in tutorials
no longer exists — current releases take positional arguments under a find subcommand and will
print a bare usage line if you use the old syntax. The output band is wide, and it is a band of
possibility: it tells you where such a shadow could fall, not where this one did. It is at its
most useful when the timestamp is independently known — from a livestream, a broadcast, a
timestamped post — and least useful when you derived the timestamp from the same photo.
GeoHints
Web only, free, and the reference for step one. It catalogues the regional giveaways by country: bollards, utility poles, traffic lights, road markings, licence plates, house numbers, sign shapes, road-numbering schemes, post boxes, driving side, even the vehicles Google mounts its cameras on. Built for GeoGuessr, which is precisely why it is organised the way an investigator needs — by visible object rather than by country.
1. Pick the object in your frame that is most likely to be regulated nationally.
Ranked by how much they narrow things: licence plate format > road sign shape and
font > utility pole construction > bollard > kerb and road marking > house numbers.
2. Open that object's page and scan the per-country plates side by side. You are
looking for a disqualifier as much as a match -- "not this country" eliminates
faster than "maybe this one".
3. Cross two independent objects before you commit to a region. A pole design shared
by six countries and a sign font shared by four may intersect at one.
4. Check the Signs subsection separately: back-of-sign colour, chevron style and
bus-stop design are all national and all usually visible in a frame that has no
readable text at all.
5. Record which object you matched on and the page you matched it against. "Bollard
type matches Portugal" is checkable; "looks Iberian" is not.
It is a crowd-built reference, so coverage is uneven — rich for Europe and the GeoGuessr-popular countries, thin for much of Africa and Central Asia, and it lags on recent sign redesigns and plate reissues. Nothing on it is evidence by itself: a match narrows the search space, and the actual geolocation still has to end at a specific place on a specific image.
QGIS and gdal_viewshed
Terrain is the fallback when there is no text in the frame. A ridgeline’s silhouette depends only on the observer’s position, so if you can trace the horizon in the photo you can test candidate positions against a DEM until one produces that profile. QGIS has carried a native Elevation Profile panel since 3.26, which is the fastest way to do it interactively.
QGIS elevation profile, for matching a horizon
1. Load a DEM (SRTM, Copernicus GLO-30, or a national LiDAR product where one exists).
2. Layer Properties > Elevation > tick "Represent elevation surface". Without this the
profile panel will not see the layer and the panel looks broken.
3. View > Elevation Profile to open the panel.
4. "Capture curve": draw a line from your candidate camera position out along the
bearing the photograph faces, past the furthest ridge.
5. Compare the plotted skyline against the photo's horizon. Use the vertical
exaggeration control to match the photo's apparent relief, then set it back to 1
before you quote any number off it.
6. Wrong candidate positions fail obviously -- a peak in the wrong order, a saddle
that should be hidden. That is the point: this step eliminates fast.
For the reverse question — “could this spot have been seen from there at all” — gdal_viewshed
answers it as a raster, which is better than eyeballing when you have many candidates:
# binary visibility from an observer 2 m above the surface, out to 15 km
# -ox/-oy are in the DEM's own CRS units, so reproject the DEM to a metric CRS first
gdal_viewshed -md 15000 -ox 499000 -oy 4283000 -oz 2 dem_utm.tif viewshed.tif
# the minimum height a target would need to be visible, rather than a yes/no mask
gdal_viewshed -om DEM -md 15000 -ox 499000 -oy 4283000 dem_utm.tif minheight.tif
# accumulate over a grid of observers: where could a photographer have stood
gdal_viewshed -om ACCUM -md 15000 -ox 499000 -oy 4283000 dem_utm.tif accum.tif
# target height matters when the subject is a mast or a roofline, not the ground
gdal_viewshed -md 20000 -tz 30 -ox 499000 -oy 4283000 dem_utm.tif mast_visible.tif
A DEM is a bare-earth or surface model depending on which one you downloaded, and the difference decides the answer: a bare-earth model will happily tell you that you can see through a forest and a city. 30 m postings smooth real ridgelines, so a profile that is close but not exact is expected and is not a match. And viewshed output is geometric — it knows nothing about haze, which is what actually limits how far a camera sees on the day in question. Reprojection, cropping and hillshading of these rasters are covered on Maps, Satellite & Street-Level Imagery.
exiftool
Before any of the above, check whether the question is already answered. Most images stripped by social platforms have nothing, but files received directly from a source, pulled from a cloud drive, or lifted off a messaging app’s media folder frequently still carry GPS.
brew install exiftool # or: apt install libimage-exiftool-perl
# GPS as a decimal pair you can paste into a map, and nothing else
exiftool -n -GPSLatitude -GPSLongitude -GPSAltitude photo.jpg
# the direction the camera was pointing, which turns a point into a line of sight
exiftool -GPSImgDirection -GPSImgDirectionRef photo.jpg
# every timestamp in the file, including the offset that tells you the local zone
exiftool -time:all -OffsetTime -OffsetTimeOriginal -a -G1 photo.jpg
# a whole directory into one CSV, to find which file in a set still has coordinates
exiftool -csv -n -GPSLatitude -GPSLongitude -DateTimeOriginal ./images > gps.csv
A GPS tag is a claim made by the device, not a fact: it can be a cached fix from the last place the phone had signal, it can be the location the file was edited rather than shot, and it can be written by hand. Treat it as a strong lead that still has to be confirmed against the frame. Full metadata, container and manipulation analysis lives on Image & Video Forensics rather than here.
Shadow and sun tools at a glance
| Tool | What it does |
|---|---|
| SunCalc | Sun position, azimuth, altitude and shadow length for any place, date and time. The workhorse. |
| ShadowMap | 3D buildings with cast shadows rendered at a chosen time. |
| ShadeMap | Global shadow simulation including terrain as well as buildings. |
| Shadow Finder | Inverts the problem: given shadow length and a timestamp, maps every point on earth where that shadow is possible. |
| suncalc-py | The SunCalc model as a Python library, for sweeping many candidates. |
| pysolar | Independent solar-position implementation, for cross-checking. |
Visual reference
GeoHints catalogues the regional details that narrow a location — bollards, traffic lights, road markings, utility poles, licence plates — by country.
Bellingcat’s OpenStreetMap Search and Spot both let you search for relationships between features — “a church within 200m of a bridge over a river” — which is how you turn a described scene into candidate coordinates. The Overpass queries underneath that idea are on Maps, Satellite & Street-Level Imagery.
Tool reference
| Tool | What it does | Cost |
|---|---|---|
| Bellingcat OpenStreetMap Search | A user interface to search OpenStreetMap data for features in proximity to each other. | free |
| GeoHints | GeoHints is a website that provides information about things like traffic lights, utility poles, bollards etc. for different regions of the world to help… | free |
| GeoNames | The GeoNames geographical database covers all countries and contains over eleven million place names that are available for download free of charge… | free |
| Photo-Map.RU | Geotagged VK posts. | free |
| Spot | A natural language interface for querying the OpenStreetMap database to find locations which meet the search criteria described by the user. | free |
Pitfalls
- Satellite imagery has a date. A building present in the photo and absent from imagery may simply be newer, or demolished. Check the capture date and look for historical imagery.
- Terrain matching defeats you at low elevation. Flat terrain has no ridgeline to match, and a 30 m DEM will not reproduce a 10 m rise.
- Shadow work needs the true timezone, including whether DST was in force on that date in that country, which is a different question from whether it is in force now.
- One azimuth-and-elevation pair means two date windows. Reporting the first one you find, and not the second, is the most common way a chronolocation is wrong while being arithmetically correct.
- Circular reasoning. If the date came from the caption, the sun cannot confirm the caption. It can only confirm that the caption is internally consistent, which is a much weaker statement.
- Publishing a precise location endangers people. For conflict imagery especially, consider whether the coordinates need to be public.
- Plausible is not confirmed. A location that fits is a hypothesis. Confirmation means a specific feature matching in a specific place, named, so that someone else can go and disagree.
Worked example
One datum: a photograph of a public square. No metadata, no caption beyond a claim that it was taken “in September”. A lamp standard in the foreground casts a clean shadow across flat paving.
- Inventory and narrow. The bollards, the kerb profile and the black-on-white house numbers go into GeoHints object by object. The sign font and the bollard type intersect on Portugal; a partial bus-stop livery in the background does not contradict it.
- Anchor. A shopfront name is legible after a crop and upscale. Searching the name plus “Lisboa” returns one address on a named square, and Street View from the same corner reproduces the building line, the arcade spacing and the lamp standard. Candidate coordinate: 38.7075, -9.1364.
- Measure. The lamp standard matches a documented 5.00 m type and its shadow, scaled against the paving slabs, runs 8.20 m. The shadow’s bearing, taken off the satellite image along the same paving joint, is 062 degrees.
- Convert.
atan(5.00 / 8.20)= 31.37 degrees solar elevation. Sun bearing =(62 + 180) mod 360= 242 degrees, so the sun is in the west-south-west and this is an afternoon photograph. - Sweep the year. The
suncalcloop above, at the candidate coordinate, with a 1-degree azimuth and 0.5-degree elevation tolerance, returns seven matching instants in all of 2026, in two clusters:
2026-03-21 16:00 UTC az 241.6 elev 31.0
2026-03-22 16:00 UTC az 242.0 elev 31.2
2026-03-23 16:00 UTC az 242.4 elev 31.5
2026-03-24 16:00 UTC az 242.8 elev 31.7
2026-09-20 15:45 UTC az 242.1 elev 31.8
2026-09-21 15:45 UTC az 241.8 elev 31.5
2026-09-22 15:45 UTC az 241.6 elev 31.1
- Convert to local time, carefully. Portugal moved to UTC+1 on 29 March 2026 and back on 25 October. The March window is therefore 16:00 local, and the September window is 16:45 local. Same sun, different clock, entirely because of a date the arithmetic knows nothing about.
- Break the tie with something that is not the sun. The trees on the square are in full leaf and the café terrace is laid out — both argue September over the third week of March. The caption’s “September” is now consistent with the image rather than assumed by it, because the sun was computed independently and the foliage was the tiebreaker.
- Cross-check the model. The same instant through
pysolarreturns an elevation within a tenth of a degree ofsuncalc’s, which confirms the inputs rather than the conclusion.
What you can assert: a square identified by a named shopfront and reproduced building geometry, and an afternoon sun position consistent with 20–22 September 2026, around 16:45 local time, with 21–24 March as a rejected alternative and the reason for rejecting it stated.
What would falsify it: a shopfront that was at a different address on that date, paving that was relaid between the Street View capture and the photograph (which would break the shadow measurement, not the location), a lamp standard of a different height, or any evidence that the foliage argument is wrong. The measurement that carries the most risk is the 8.20 m — a 10 per cent error there moves the elevation by roughly 2.5 degrees and the window by several days, so quote it with that tolerance or do not quote a day at all.
Broader catalogues
More tools
Further tools for this area from the OSINT Newsletter Tools Library (Geolocation and Maps OSINT), excluding those already listed above.
| Tool | What it does |
|---|---|
| Geolocation Estimation | A research demonstrator from TIB that uses deep learning to estimate where a photograph was taken based on visual and geographic… |
Sources
Both catalogues below are maintained by other people and are considerably larger than this page. Use them as the canonical index; this sheet is a working route through them.
- Bellingcat’s Online Investigation Toolkit — ~340 tools, each with its own review page covering cost, difficulty, requirements and limitations.
- OSINT Newsletter Tools Library — ~280 tools, organised by investigative goal.
Neither publishes a licence, so nothing here is copied from them: tool names, one-line descriptions, cost flags and links are catalogue facts, and the method and commentary are this site’s own. See credits.