daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

geolocation.md (28016B)


      1 ---
      2 title: "Geolocation & Chronolocation"
      3 description: "Work out where a photo was taken, and when, from shadows, sun position, terrain and visible detail."
      4 category: osint
      5 subcategory: "Geospatial"
      6 tags: [osint, geolocation, chronolocation, shadows, verification]
      7 tools: [suncalc, shadowfinder, pysolar, shadowmap, shademap, geohints, qgis, gdal, exiftool]
      8 difficulty: advanced
      9 updated: 2026-10-04
     10 references:
     11   - name: "Bellingcat's Online Investigation Toolkit"
     12     url: "https://bellingcat.gitbook.io/toolkit"
     13     author: "Bellingcat"
     14     license: none
     15     relation: derived
     16     note: "Tool catalogue: names, descriptions, cost flags and links for this area."
     17   - name: "OSINT Newsletter Tools Library"
     18     url: "https://tools.osintnewsletter.com"
     19     author: "The OSINT Newsletter"
     20     license: none
     21     relation: derived
     22     note: "Second tool catalogue, cross-checked against the above."
     23 ---
     24 
     25 ## What this covers
     26 
     27 Placing an image on the earth and in time without metadata. The place comes from matching what is
     28 in the frame against the world; the time comes from the sun, which is the one thing in the picture
     29 whose behaviour is exactly calculable. A hard geolocation is hours of work, and the chronolocation
     30 that follows it is arithmetic you can show someone.
     31 
     32 ## Method
     33 
     34 1. **Inventory the frame.** List everything identifiable: road markings, utility pole design,
     35    plug sockets, licence-plate format, language and script on signage, vegetation, kerb style,
     36    architecture, mountain profile.
     37 2. **Narrow the region.** Those details constrain the country or region long before they give you a
     38    point. Pole and bollard design alone often gets you to a handful of countries.
     39 3. **Find a searchable anchor.** A business name, a phone number on a van, a street name, a bus
     40    route number. One readable sign collapses the search; spend your effort on making one legible
     41    before you spend it on scanning imagery.
     42 4. **Match terrain.** Ridgelines are fingerprints and are visible from tens of kilometres away.
     43    This is the step that works when there is no signage at all, and the step that fails on flat
     44    ground.
     45 5. **Confirm with imagery.** Satellite and street view from the era of the photo. Match building
     46    footprints and the relative geometry of fixed objects, not the general vibe of the scene.
     47 6. **Measure the shadow, then compute.** Direction gives you the sun's azimuth, the length-to-height
     48    ratio gives you its elevation. Together they give a time of day and, usually, two candidate date
     49    windows per year rather than one.
     50 7. **Say what would falsify it.** A geolocation you cannot break is one nobody checked. Name the
     51    feature that would have to be in the wrong place for your answer to be wrong.
     52 
     53 The judgement calls: decide early whether you are doing signage work or terrain work, because they
     54 need different tools and mixing them wastes hours. Decide whether the date is given or derived —
     55 if you take the date from the caption and then use the sun to "confirm" the caption, you have
     56 proved nothing. And decide whether a precise coordinate should be published at all, before you
     57 find one.
     58 
     59 ## The shadow arithmetic
     60 
     61 A vertical object of height `h` casting a shadow of length `s` on level ground fixes the sun's
     62 elevation angle:
     63 
     64 ```text
     65 tan(elevation) = h / s
     66 
     67 h = 5.00 m, s = 8.20 m
     68 elevation = atan(5.00 / 8.20) = atan(0.60976) = 31.37 degrees
     69 
     70 check: 5.00 / tan(31.37 deg) = 5.00 / 0.60976 = 8.20 m
     71 ```
     72 
     73 The shadow points directly away from the sun, so the sun's compass bearing is the shadow's bearing
     74 plus 180:
     75 
     76 ```text
     77 shadow bearing measured off the image : 062 deg
     78 sun azimuth                           : (062 + 180) mod 360 = 242 deg
     79 ```
     80 
     81 Three things break this. The ground must be level — a shadow running downhill is longer than the
     82 arithmetic expects and will push your elevation too low. The object must be vertical and you must
     83 be measuring its true height, not its foreshortened height in the frame. And `elevation` here is
     84 geometric; at elevations below about 5 degrees atmospheric refraction lifts the apparent sun by
     85 roughly half a degree, which matters at sunrise and sunset and nowhere else.
     86 
     87 Then the part people get wrong: an azimuth and elevation pair does **not** identify one date. The
     88 sun retraces its declination on the way out of winter and on the way back in, so almost every
     89 pair matches two windows in the year, roughly symmetric about the solstice. Expect two answers and
     90 rule one out with something other than the sun — foliage, snow, an event in the background, the
     91 clothes people are wearing.
     92 
     93 ## Key tools
     94 
     95 ### SunCalc
     96 
     97 Web only, free, no account. Give it a coordinate and a date and it draws the sun's track for that
     98 day with the numbers underneath: altitude, azimuth, and a **shadow length** readout for an object
     99 height you type in. That last field is why it beats the generic ephemeris sites — you can go
    100 straight from the measurement you took off the image to the time that produces it, without
    101 converting anything.
    102 
    103 ```text
    104 1. Enter the coordinate in "Set Lat/Lon", or drag the map pin onto the spot.
    105 2. Set month and date in the dropdowns. Note the timezone line (TZ) it reports; everything
    106    downstream is in that zone, and it is the usual source of a one-hour error.
    107 3. Drag the time slider and watch two readouts: "Azimuth" against your measured sun bearing,
    108    and "Shadow length [m]" against the shadow you measured, with "at an object level [m]"
    109    set to the object's real height.
    110 4. When both match at once, you have a time. When only the azimuth matches, you have two
    111    times -- morning and afternoon mirror each other, and length is what separates them.
    112 5. "Reverse Calculation" works the other way: fix the shadow and ask for the time.
    113 6. Capture the page URL. The state lives in the fragment --
    114    #/<lat>,<lon>,<zoom>/<YYYY.MM.DD>/<HH:MM>/<object height> -- so the link reproduces the
    115    exact reading rather than just the tool.
    116 ```
    117 
    118 The azimuth it reports is a compass bearing measured clockwise from north, which is what you
    119 measured off the image, so no conversion is needed here. Read-offs from a slider are worth about a
    120 minute of precision at best; quote a window, not a timestamp. And the whole result is conditional
    121 on the coordinate — a 50 m position error barely moves the azimuth, but a wrong *date* moves it by
    122 degrees per week away from the solstices, which is exactly where the two-candidate-window problem
    123 bites.
    124 
    125 ### ShadeMap and Shadowmap
    126 
    127 Also web only. These solve the opposite problem: when the shadow in your image is cast by a
    128 building or a ridge rather than by the subject, you cannot measure an object height, so SunCalc's
    129 arithmetic has nothing to chew on. These render the shadows that real geometry casts at a chosen
    130 moment and you match the *pattern* instead.
    131 
    132 ```text
    133 ShadeMap (shademap.app)
    134   - loads OpenStreetMap building footprints plus a terrain model, so both urban and
    135     mountain shadows are simulated
    136   - date and time slider at the bottom; the shadow polygons redraw live
    137   - the sun-exposure analytics (hourly / daily / annual) are a solar-study feature,
    138     not an investigative one -- ignore them for this work
    139   - free for interactive use on the site; the paid tiers are for embedding the engine
    140     in your own map, not for using it here
    141 
    142 Shadowmap (shadowmap.org)
    143   - 3D buildings with cast shadows, better for dense city blocks
    144   - set the date and time, then orbit the camera to the photographer's approximate
    145     position and compare the shadow edges against the frame
    146   - sign-in gates some of the time controls
    147 
    148 What to capture either way: a screenshot at the matched time, the coordinate, the
    149 date, the stated timezone, and the building or ridge you matched on.
    150 ```
    151 
    152 Building shadows are only as good as the building heights in OpenStreetMap, which are frequently
    153 absent, guessed, or recorded for a structure that has since been extended. Terrain shadows are more
    154 trustworthy because the DEM is measured. Neither knows about trees, awnings, scaffolding or a
    155 crane that was on site that month — so an unexplained shadow is a reason to doubt your position
    156 before it is a reason to doubt the tool.
    157 
    158 ### suncalc (Python)
    159 
    160 The same model as the website, as a library, which is what you want once you are checking more than
    161 one candidate. Give it a coordinate and a timestamp and it returns the sun's position; loop it over
    162 a year and you get every moment that matches your measurement, which is the honest way to find both
    163 date windows instead of stopping at the first one.
    164 
    165 ```bash
    166 pipx install suncalc    # or: pip install suncalc pandas
    167 ```
    168 
    169 ```python
    170 import math, datetime
    171 import pandas as pd
    172 from suncalc import get_position, get_times
    173 
    174 lat, lon = 38.7075, -9.1364          # candidate coordinate
    175 height, shadow = 5.00, 8.20          # metres, measured off the image
    176 shadow_bearing = 62.0                # degrees, measured off the image
    177 
    178 # the two numbers the image gives you
    179 elevation = math.degrees(math.atan(height / shadow))   # 31.37
    180 sun_bearing = (shadow_bearing + 180) % 360             # 242.0
    181 
    182 def sun(dt):
    183     # suncalc returns radians, and its azimuth is measured from SOUTH toward west --
    184     # add 180 degrees to get the compass bearing you measured off the image
    185     p = get_position(pd.Timestamp(dt), lon, lat)
    186     return (math.degrees(p["azimuth"]) + 180) % 360, math.degrees(p["altitude"])
    187 
    188 # sweep the whole year at 5-minute resolution: this is what exposes the second window
    189 start = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
    190 for step in range(0, 365 * 288):
    191     dt = start + datetime.timedelta(minutes=5 * step)
    192     az, alt = sun(dt)
    193     if abs(az - sun_bearing) < 1.0 and abs(alt - elevation) < 0.5:
    194         print(f"{dt:%Y-%m-%d %H:%M} UTC  az {az:6.1f}  elev {alt:5.1f}")
    195 
    196 # sanity-check the day itself: if your candidate time is after dusk, the coordinate is wrong
    197 t = get_times(pd.Timestamp("2026-03-22 12:00:00"), lon, lat)
    198 print("sunrise", t["sunrise"], "solar noon", t["solar_noon"], "sunset", t["sunset"])
    199 ```
    200 
    201 Two conventions to hold onto, because getting either wrong silently produces a plausible, wrong
    202 answer. Azimuth comes back in radians measured from south, so the `+ 180` above is not optional.
    203 Altitude is geometric and ignores refraction. Pass timezone-aware UTC datetimes and convert to
    204 local time at the very end, after you have checked whether the location observes DST on that date
    205 — a shadow matched in March against a September local time is the most common way this analysis
    206 goes quietly wrong. The package is thin and has not changed since 2023; that is fine for a model
    207 of celestial mechanics, and it is not a sign of anything.
    208 
    209 ### pysolar
    210 
    211 A second implementation, which is the point of using it: run the same coordinate and timestamp
    212 through both and you are checking your arithmetic rather than one library's. It is also the more
    213 natural fit when you want irradiance or want to sweep without pandas in the way.
    214 
    215 ```bash
    216 pipx install pysolar    # or: pip install pysolar
    217 ```
    218 
    219 ```python
    220 import datetime
    221 from pysolar.solar import get_altitude, get_azimuth
    222 
    223 lat, lon = 38.7075, -9.1364
    224 when = datetime.datetime(2026, 6, 15, 12, 30, tzinfo=datetime.timezone.utc)
    225 
    226 # pysolar's azimuth is already a compass bearing, clockwise from north -- no offset needed
    227 print("altitude", get_altitude(lat, lon, when))   # 56.80
    228 print("azimuth ", get_azimuth(lat, lon, when))    # 216.45
    229 
    230 # cross-check against suncalc for the same instant: 56.79 / 216.36
    231 # agreement to a tenth of a degree means the inputs are right; a whole-degree gap
    232 # almost always means one of the two got a naive datetime and assumed local time
    233 ```
    234 
    235 `get_altitude` applies a refraction correction by default, which is why it sits a hundredth of a
    236 degree off suncalc's geometric value near the zenith and further off near the horizon — at 5
    237 degrees elevation the two will differ by about half a degree, and pysolar is the one closer to what
    238 a camera actually saw. Passing a naive `datetime` is the failure mode: it will be interpreted as
    239 UTC and you will get an answer that looks fine and is hours wrong.
    240 
    241 ### ShadowFinder
    242 
    243 Bellingcat's inversion of the problem, for when you have no candidate coordinate at all. Feed it an
    244 object height, a shadow length and a UTC timestamp and it maps every point on earth where a shadow
    245 of that ratio is possible at that instant — a band, not a point, but a band you can intersect with
    246 everything else you know.
    247 
    248 ```bash
    249 pipx install shadowfinder    # or: pip install shadowfinder
    250 
    251 # object height, shadow length, date, time -- POSITIONAL, in that order
    252 shadowfinder find 5 8.2 2026-03-22 16:00:00 --time_format=utc
    253 
    254 # when you already have the sun's elevation rather than a height/length pair
    255 shadowfinder find_sun 31.37 2026-03-22 16:00:00 --time_format=utc
    256 
    257 # the subcommands and their arguments, before you guess
    258 shadowfinder find --help
    259 shadowfinder find_sun --help
    260 
    261 # writes a PNG map into the current directory, named for its inputs:
    262 #   shadow_finder_20260322-160000-Utc_5_8.2.png
    263 ls shadow_finder_*.png
    264 
    265 # the timezone grid it uses for local-time input is generated once and cached
    266 shadowfinder generate_timezone_grid
    267 ```
    268 
    269 The older `--object-height / --shadow-length / --date-time` flag form that circulates in tutorials
    270 no longer exists — current releases take positional arguments under a `find` subcommand and will
    271 print a bare usage line if you use the old syntax. The output band is wide, and it is a band of
    272 *possibility*: it tells you where such a shadow could fall, not where this one did. It is at its
    273 most useful when the timestamp is independently known — from a livestream, a broadcast, a
    274 timestamped post — and least useful when you derived the timestamp from the same photo.
    275 
    276 ### GeoHints
    277 
    278 Web only, free, and the reference for step one. It catalogues the regional giveaways by country:
    279 bollards, utility poles, traffic lights, road markings, licence plates, house numbers, sign shapes,
    280 road-numbering schemes, post boxes, driving side, even the vehicles Google mounts its cameras on.
    281 Built for GeoGuessr, which is precisely why it is organised the way an investigator needs — by
    282 visible object rather than by country.
    283 
    284 ```text
    285 1. Pick the object in your frame that is most likely to be regulated nationally.
    286    Ranked by how much they narrow things: licence plate format > road sign shape and
    287    font > utility pole construction > bollard > kerb and road marking > house numbers.
    288 2. Open that object's page and scan the per-country plates side by side. You are
    289    looking for a disqualifier as much as a match -- "not this country" eliminates
    290    faster than "maybe this one".
    291 3. Cross two independent objects before you commit to a region. A pole design shared
    292    by six countries and a sign font shared by four may intersect at one.
    293 4. Check the Signs subsection separately: back-of-sign colour, chevron style and
    294    bus-stop design are all national and all usually visible in a frame that has no
    295    readable text at all.
    296 5. Record which object you matched on and the page you matched it against. "Bollard
    297    type matches Portugal" is checkable; "looks Iberian" is not.
    298 ```
    299 
    300 It is a crowd-built reference, so coverage is uneven — rich for Europe and the GeoGuessr-popular
    301 countries, thin for much of Africa and Central Asia, and it lags on recent sign redesigns and plate
    302 reissues. Nothing on it is evidence by itself: a match narrows the search space, and the actual
    303 geolocation still has to end at a specific place on a specific image.
    304 
    305 ### QGIS and gdal_viewshed
    306 
    307 Terrain is the fallback when there is no text in the frame. A ridgeline's silhouette depends only
    308 on the observer's position, so if you can trace the horizon in the photo you can test candidate
    309 positions against a DEM until one produces that profile. QGIS has carried a native **Elevation
    310 Profile** panel since 3.26, which is the fastest way to do it interactively.
    311 
    312 ```text
    313 QGIS elevation profile, for matching a horizon
    314 1. Load a DEM (SRTM, Copernicus GLO-30, or a national LiDAR product where one exists).
    315 2. Layer Properties > Elevation > tick "Represent elevation surface". Without this the
    316    profile panel will not see the layer and the panel looks broken.
    317 3. View > Elevation Profile to open the panel.
    318 4. "Capture curve": draw a line from your candidate camera position out along the
    319    bearing the photograph faces, past the furthest ridge.
    320 5. Compare the plotted skyline against the photo's horizon. Use the vertical
    321    exaggeration control to match the photo's apparent relief, then set it back to 1
    322    before you quote any number off it.
    323 6. Wrong candidate positions fail obviously -- a peak in the wrong order, a saddle
    324    that should be hidden. That is the point: this step eliminates fast.
    325 ```
    326 
    327 For the reverse question — "could this spot have been seen from there at all" — `gdal_viewshed`
    328 answers it as a raster, which is better than eyeballing when you have many candidates:
    329 
    330 ```bash
    331 # binary visibility from an observer 2 m above the surface, out to 15 km
    332 # -ox/-oy are in the DEM's own CRS units, so reproject the DEM to a metric CRS first
    333 gdal_viewshed -md 15000 -ox 499000 -oy 4283000 -oz 2 dem_utm.tif viewshed.tif
    334 
    335 # the minimum height a target would need to be visible, rather than a yes/no mask
    336 gdal_viewshed -om DEM -md 15000 -ox 499000 -oy 4283000 dem_utm.tif minheight.tif
    337 
    338 # accumulate over a grid of observers: where could a photographer have stood
    339 gdal_viewshed -om ACCUM -md 15000 -ox 499000 -oy 4283000 dem_utm.tif accum.tif
    340 
    341 # target height matters when the subject is a mast or a roofline, not the ground
    342 gdal_viewshed -md 20000 -tz 30 -ox 499000 -oy 4283000 dem_utm.tif mast_visible.tif
    343 ```
    344 
    345 A DEM is a bare-earth or surface model depending on which one you downloaded, and the difference
    346 decides the answer: a bare-earth model will happily tell you that you can see through a forest and
    347 a city. 30 m postings smooth real ridgelines, so a profile that is close but not exact is expected
    348 and is not a match. And viewshed output is geometric — it knows nothing about haze, which is what
    349 actually limits how far a camera sees on the day in question. Reprojection, cropping and
    350 hillshading of these rasters are covered on
    351 [Maps, Satellite & Street-Level Imagery](/sheets/osint/maps-and-satellite-imagery).
    352 
    353 ### exiftool
    354 
    355 Before any of the above, check whether the question is already answered. Most images stripped by
    356 social platforms have nothing, but files received directly from a source, pulled from a cloud
    357 drive, or lifted off a messaging app's media folder frequently still carry GPS.
    358 
    359 ```bash
    360 brew install exiftool          # or: apt install libimage-exiftool-perl
    361 
    362 # GPS as a decimal pair you can paste into a map, and nothing else
    363 exiftool -n -GPSLatitude -GPSLongitude -GPSAltitude photo.jpg
    364 
    365 # the direction the camera was pointing, which turns a point into a line of sight
    366 exiftool -GPSImgDirection -GPSImgDirectionRef photo.jpg
    367 
    368 # every timestamp in the file, including the offset that tells you the local zone
    369 exiftool -time:all -OffsetTime -OffsetTimeOriginal -a -G1 photo.jpg
    370 
    371 # a whole directory into one CSV, to find which file in a set still has coordinates
    372 exiftool -csv -n -GPSLatitude -GPSLongitude -DateTimeOriginal ./images > gps.csv
    373 ```
    374 
    375 A GPS tag is a claim made by the device, not a fact: it can be a cached fix from the last place the
    376 phone had signal, it can be the location the file was edited rather than shot, and it can be
    377 written by hand. Treat it as a strong lead that still has to be confirmed against the frame. Full
    378 metadata, container and manipulation analysis lives on
    379 [Image & Video Forensics](/sheets/osint/image-video-forensics) rather than here.
    380 
    381 ## Shadow and sun tools at a glance
    382 
    383 | Tool | What it does |
    384 | --- | --- |
    385 | [SunCalc](https://www.suncalc.org/) | Sun position, azimuth, altitude and shadow length for any place, date and time. The workhorse. |
    386 | [ShadowMap](https://shadowmap.org/) | 3D buildings with cast shadows rendered at a chosen time. |
    387 | [ShadeMap](https://shademap.app/) | Global shadow simulation including terrain as well as buildings. |
    388 | [Shadow Finder](https://github.com/bellingcat/ShadowFinder) | Inverts the problem: given shadow length and a timestamp, maps every point on earth where that shadow is possible. |
    389 | [suncalc-py](https://github.com/kylebarron/suncalc-py) | The SunCalc model as a Python library, for sweeping many candidates. |
    390 | [pysolar](https://pysolar.readthedocs.io/) | Independent solar-position implementation, for cross-checking. |
    391 
    392 ## Visual reference
    393 
    394 [GeoHints](https://geohints.com/) catalogues the regional details that narrow a location — bollards,
    395 traffic lights, road markings, utility poles, licence plates — by country.
    396 
    397 [Bellingcat's OpenStreetMap Search](https://osm-search.bellingcat.com/) and
    398 [Spot](https://www.findthatspot.io/) both let you search for *relationships* between features —
    399 "a church within 200m of a bridge over a river" — which is how you turn a described scene into
    400 candidate coordinates. The Overpass queries underneath that idea are on
    401 [Maps, Satellite & Street-Level Imagery](/sheets/osint/maps-and-satellite-imagery).
    402 
    403 ## Tool reference
    404 
    405 | Tool | What it does | Cost |
    406 | --- | --- | --- |
    407 | [Bellingcat OpenStreetMap Search](https://osm-search.bellingcat.com/) | A user interface to search OpenStreetMap data for features in proximity to each other. | free |
    408 | [GeoHints](https://geohints.com/) | 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 |
    409 | [GeoNames](http://www.geonames.org/) | The GeoNames geographical database covers all countries and contains over eleven million place names that are available for download free of charge… | free |
    410 | [Photo-Map.RU](http://photo-map.ru/) | Geotagged VK posts. | free |
    411 | [Spot](https://www.findthatspot.io/) | A natural language interface for querying the OpenStreetMap database to find locations which meet the search criteria described by the user. | free |
    412 
    413 ## Pitfalls
    414 
    415 - **Satellite imagery has a date.** A building present in the photo and absent from imagery may
    416   simply be newer, or demolished. Check the capture date and look for historical imagery.
    417 - **Terrain matching defeats you at low elevation.** Flat terrain has no ridgeline to match, and a
    418   30 m DEM will not reproduce a 10 m rise.
    419 - **Shadow work needs the true timezone**, including whether DST was in force on that date in that
    420   country, which is a different question from whether it is in force now.
    421 - **One azimuth-and-elevation pair means two date windows.** Reporting the first one you find, and
    422   not the second, is the most common way a chronolocation is wrong while being arithmetically
    423   correct.
    424 - **Circular reasoning.** If the date came from the caption, the sun cannot confirm the caption.
    425   It can only confirm that the caption is internally consistent, which is a much weaker statement.
    426 - **Publishing a precise location endangers people.** For conflict imagery especially, consider
    427   whether the coordinates need to be public.
    428 - **Plausible is not confirmed.** A location that fits is a hypothesis. Confirmation means a
    429   specific feature matching in a specific place, named, so that someone else can go and disagree.
    430 
    431 ## Worked example
    432 
    433 One datum: a photograph of a public square. No metadata, no caption beyond a claim that it was
    434 taken "in September". A lamp standard in the foreground casts a clean shadow across flat paving.
    435 
    436 1. **Inventory and narrow.** The bollards, the kerb profile and the black-on-white house numbers
    437    go into GeoHints object by object. The sign font and the bollard type intersect on Portugal;
    438    a partial bus-stop livery in the background does not contradict it.
    439 2. **Anchor.** A shopfront name is legible after a crop and upscale. Searching the name plus
    440    "Lisboa" returns one address on a named square, and Street View from the same corner reproduces
    441    the building line, the arcade spacing and the lamp standard. Candidate coordinate:
    442    **38.7075, -9.1364**.
    443 3. **Measure.** The lamp standard matches a documented 5.00 m type and its shadow, scaled against
    444    the paving slabs, runs 8.20 m. The shadow's bearing, taken off the satellite image along the
    445    same paving joint, is 062 degrees.
    446 4. **Convert.** `atan(5.00 / 8.20)` = **31.37 degrees** solar elevation. Sun bearing =
    447    `(62 + 180) mod 360` = **242 degrees**, so the sun is in the west-south-west and this is an
    448    afternoon photograph.
    449 5. **Sweep the year.** The `suncalc` loop above, at the candidate coordinate, with a 1-degree
    450    azimuth and 0.5-degree elevation tolerance, returns seven matching instants in all of 2026, in
    451    two clusters:
    452 
    453 ```text
    454 2026-03-21 16:00 UTC   az 241.6   elev 31.0
    455 2026-03-22 16:00 UTC   az 242.0   elev 31.2
    456 2026-03-23 16:00 UTC   az 242.4   elev 31.5
    457 2026-03-24 16:00 UTC   az 242.8   elev 31.7
    458 
    459 2026-09-20 15:45 UTC   az 242.1   elev 31.8
    460 2026-09-21 15:45 UTC   az 241.8   elev 31.5
    461 2026-09-22 15:45 UTC   az 241.6   elev 31.1
    462 ```
    463 
    464 6. **Convert to local time, carefully.** Portugal moved to UTC+1 on 29 March 2026 and back on 25
    465    October. The March window is therefore **16:00 local**, and the September window is **16:45
    466    local**. Same sun, different clock, entirely because of a date the arithmetic knows nothing
    467    about.
    468 7. **Break the tie with something that is not the sun.** The trees on the square are in full leaf
    469    and the café terrace is laid out — both argue September over the third week of March. The
    470    caption's "September" is now consistent with the image rather than assumed by it, because the
    471    sun was computed independently and the foliage was the tiebreaker.
    472 8. **Cross-check the model.** The same instant through `pysolar` returns an elevation within a
    473    tenth of a degree of `suncalc`'s, which confirms the inputs rather than the conclusion.
    474 
    475 What you can assert: a square identified by a named shopfront and reproduced building geometry, and
    476 an afternoon sun position consistent with **20–22 September 2026, around 16:45 local time**, with
    477 21–24 March as a rejected alternative and the reason for rejecting it stated.
    478 
    479 What would falsify it: a shopfront that was at a different address on that date, paving that was
    480 relaid between the Street View capture and the photograph (which would break the shadow
    481 measurement, not the location), a lamp standard of a different height, or any evidence that the
    482 foliage argument is wrong. The measurement that carries the most risk is the 8.20 m — a 10 per cent
    483 error there moves the elevation by roughly 2.5 degrees and the window by several days, so quote it
    484 with that tolerance or do not quote a day at all.
    485 
    486 ## Broader catalogues
    487 
    488 - [Geolocation and Maps OSINT](https://tools.osintnewsletter.com/tool-categories/geolocation-and-maps-osint)
    489 
    490 
    491 ## More tools
    492 
    493 Further tools for this area from the OSINT Newsletter Tools Library ([Geolocation and Maps OSINT](https://tools.osintnewsletter.com/tool-categories/geolocation-and-maps-osint)), excluding those already listed above.
    494 
    495 | Tool | What it does |
    496 | --- | --- |
    497 | [Geolocation Estimation](https://labs.tib.eu/geoestimation/) | A research demonstrator from TIB that uses deep learning to estimate where a photograph was taken based on visual and geographic… |
    498 
    499 ## Sources
    500 
    501 Both catalogues below are maintained by other people and are considerably larger than
    502 this page. Use them as the canonical index; this sheet is a working route through them.
    503 
    504 - [Bellingcat's Online Investigation Toolkit](https://bellingcat.gitbook.io/toolkit) — ~340 tools, each with its own
    505   review page covering cost, difficulty, requirements and limitations.
    506 - [OSINT Newsletter Tools Library](https://tools.osintnewsletter.com) — ~280 tools, organised by investigative goal.
    507 
    508 Neither publishes a licence, so nothing here is copied from them: tool names, one-line
    509 descriptions, cost flags and links are catalogue facts, and the method and commentary are
    510 this site's own. See [credits](/credits).