OSINT^:OSINT

Maps, Satellite & Street-Level Imagery

Choose the right imagery source for the question, find historical coverage, and work with the data in QGIS.

intermediateupdated 2026-10-04qgis · gdal · ogr2ogr · overpass

What this covers

Which imagery to use, how to get at older coverage, and the street-level sources beyond Google. The main skill is matching the source to the question — resolution, revisit frequency and archive depth trade off against each other and no single provider wins on all three.

Method

  1. Fix the question before the source. “Has this compound grown since 2019” and “what is the writing on that sign” are answered by different satellites, and no source answers both.
  2. Bound the area and the dates. A coordinate with no radius and a year with no window will produce thousands of scenes and no answer. Write both down first.
  3. Start free and coarse. Sentinel-2 at 10m settles most change-detection questions. Spend high-resolution effort only on confirming the specific thing it points at.
  4. Record the capture date of every frame you use, from the source’s own metadata rather than the page you found it on. An undated image supports no time-sensitive claim.
  5. Cross-check the base layer. Two providers disagree on building footprints, place names and borders. Say which one you used, and look at a second before concluding a feature is new.
  6. Measure in a metric CRS. Distances taken in degrees are wrong by a factor that varies with latitude, and the tool will not warn you.
  7. Pivot to street level last. Once satellite imagery has given you candidates, Mapillary or Street View either confirms the ground detail or kills the candidate in seconds.

Choosing a source

QuestionSource
What does this place look like in detail?Google Earth Pro, Esri World Imagery — sub-metre, but infrequent
What changed between two dates?Sentinel-2 (5-day revisit, 10m), Landsat (16-day, 30m, back to 1972)
What did it look like years ago?Google Earth Pro’s historical slider, Landsat archive
Was there a fire / flood / new construction?Sentinel-2 false-colour composites, NASA FIRMS
What is at street level?Google Street View, Mapillary, KartaView, Yandex Panoramas
What features exist here, as data?OpenStreetMap via Overpass

Free high-cadence optical imagery bottoms out around 10m per pixel. Anything finer is commercial and usually costs real money, so plan around Sentinel for change detection and reserve high-res for confirming a specific thing.

Imagery sources at a glance

Resolution is the number people quote and the least useful of the four. Revisit and archive depth decide whether a question is answerable at all, and scriptability decides whether you can answer it for fifty coordinates instead of one.

SourceResolutionRevisitArchive back toScriptable access
Sentinel-2 (Copernicus)10m visible~5 days2015OData and STAC, free, token for download
Sentinel-1 (radar)5x20m~6 days2014same OData catalogue
Landsat 8/930m, 15m pan16 days each1972 across the seriesLandsatLook STAC, no key for search
MODIS / VIIRS250m–1kmsub-daily2000 / 2012GIBS WMTS and WMS, no key
Google Earth Prosub-metreirregular, years apartvaries by place, often 1985none; desktop export only
Esri World Imagerysub-metreirregularcurrent only, no sliderXYZ tile URL
Mapillarystreet levelcontributor-driven2014Graph API, free token
OpenStreetMapvector, not imagerycontinuousfull edit historyOverpass API, no key

Two consequences worth internalising. Nothing free gives you both sub-metre resolution and a dated archive, which is why high-resolution work means Google Earth Pro screenshots with the status-bar date, and change detection means Sentinel. And the published revisit figures are orbital, not usable: see the cloud arithmetic in the worked example below, where a nominal five-day revisit yielded eighteen scenes in a month and two worth opening.

Historical imagery

Google Earth Pro is free desktop software and its historical imagery slider is the most accessible archive of high-resolution coverage. Note the imagery date shown at the bottom — it is the single most important piece of context and the most commonly ignored.

Landsat goes back to 1972 and is the only free option for multi-decade change. Browse it via EarthExplorer, covered below, or query the same archive through the LandsatLook STAC API, which needs no account.

Street-level beyond Google

Google’s coverage is deep but not universal, and its capture dates are sometimes years old:

  • Mapillary — crowd-sourced, often covers roads Google skipped, frequently more recent.
  • KartaView — similar model, strong in parts of Europe.
  • Yandex Panoramas — the best coverage across Russia and Central Asia by a wide margin.
  • Baidu Total View — mainland China.

Always check several; a street with no Google coverage often has Mapillary imagery.

Key tools

Overpass API

OpenStreetMap’s query interface, and the reason OSM is a database rather than a picture. Give it a tag combination and a bounding box and it returns the features — which is how you turn “a church with a red roof next to a roundabout, somewhere in this province” into a list of candidates. Overpass Turbo is the browser front end; the API behind it takes curl, and automating it is what makes a large search tractable.

# the public endpoint rejects requests without a User-Agent — this is the usual first failure
UA='osint-research/1.0'
OVERPASS='https://overpass-api.de/api/interpreter'

# every pharmacy in a bounding box (south,west,north,east)
curl -s -A "$UA" "$OVERPASS" --data-urlencode \
  'data=[out:json][timeout:25];node["amenity"="pharmacy"](52.37,4.88,52.38,4.90);out body;'

# bridges over waterways — matching a described scene to candidate locations
curl -s -A "$UA" "$OVERPASS" --data-urlencode \
  'data=[out:json][timeout:60];way["bridge"="yes"](52.3,4.8,52.4,4.95);out geom;' \
  | jq '.elements | length'

# two features near each other: a mosque within 200m of a school
curl -s -A "$UA" "$OVERPASS" --data-urlencode \
  'data=[out:json][timeout:90];
   way["amenity"="place_of_worship"]["religion"="muslim"](35.6,51.3,35.8,51.5)->.w;
   node(around.w:200)["amenity"="school"];out center;'

# cell masts, which are mapped far more completely than people expect
curl -s -A "$UA" "$OVERPASS" --data-urlencode \
  'data=[out:json];node["man_made"="mast"]["tower:type"="communication"](48.1,16.3,48.3,16.5);out center;'

# count first, then fetch: a national-scale query that returns nothing costs you nothing
curl -s -A "$UA" "$OVERPASS" --data-urlencode \
  'data=[out:csv(::count)][timeout:120];area["ISO3166-1"="NL"]->.a;node["amenity"="fuel"](area.a);out count;'

# straight to a file your GIS can open
curl -s -A "$UA" "$OVERPASS" --data-urlencode \
  'data=[out:json][timeout:60];node["aeroway"="aerodrome"](34.0,43.0,35.0,44.5);out center;' \
  -o aerodromes.json

Four forms of the query that earn their keep once the search is more than one box. nwr matches nodes, ways and relations in one pass, which matters because a feature mapped as a node in one country is a way in the next; a node[...] query silently misses half of them. The [bbox:...] setting applies to every statement that carries no explicit box, so an exploratory query stops being a wall of repeated coordinates. out:csv with named fields lands in awk or a spreadsheet without a jq filter in between. And (newer:...) asks OSM’s own edit history what changed, which is a change-detection signal that costs nothing and arrives before any satellite pass.

# CSV with chosen fields: header line on, comma separator. Note the renaming --
# ::id comes back as the column @id, which is what you grep for afterwards
curl -s -A "$UA" "$OVERPASS" --data-urlencode \
  'data=[out:csv(::id,::type,::lat,::lon,"name";true;",")][timeout:30];
   node["amenity"="pharmacy"](52.37,4.88,52.38,4.90);out center;'
# @id,@type,@lat,@lon,name
# 1819064252,node,52.3723707,4.8940844,Dam Apotheek
# 2720875314,node,52.3783767,4.8823525,Medicijnman Apotheek Jordaan

# nwr, so a feature mapped as a way somewhere is not missed. Counting first:
curl -s -A "$UA" "$OVERPASS" --data-urlencode \
  'data=[out:csv(::count)][timeout:60];area["ISO3166-1"="NL"]->.a;
   nwr["amenity"="fuel"](area.a);out count;'
# @count
# 4117

# a global bbox in the settings, so each statement inherits it
curl -s -A "$UA" "$OVERPASS" --data-urlencode \
  'data=[bbox:52.3730,4.8900,52.3740,4.8915][out:json][timeout:25];nwr["historic"];out center;'

# what OSM itself says changed: buildings touched since a date, with edit metadata
curl -s -A "$UA" "$OVERPASS" --data-urlencode \
  'data=[out:json][timeout:60];
   way["building"](52.3730,4.8900,52.3740,4.8915)(newer:"2020-01-01T00:00:00Z");out meta center;' \
  | jq -r '.elements[] | "\(.timestamp)  v\(.version)  \(.id)"'

Getting the result into a GIS is the step people do by hand and should not. Overpass will emit OSM XML, which GDAL’s OSM driver reads directly, so one curl and one ogr2ogr produce a GeoPackage. The recurse-down operator > is what makes the XML usable: without (._;>;) you get ways with no node coordinates and every geometry comes out empty.

# ways plus the nodes that define them, as OSM XML
curl -s -A "$UA" "$OVERPASS" --data-urlencode \
  'data=[out:xml][timeout:60];way["building"](52.3730,4.8900,52.3740,4.8915);(._;>;);out meta;' \
  -o area.osm

# the driver exposes five fixed layers: points, lines, multilinestrings,
# multipolygons, other_relations. Closed building ways land in multipolygons.
ogr2ogr -f GPKG area.gpkg area.osm multipolygons -nln buildings

# tag filtering needs the SQLite dialect, because tags arrive as one other_tags blob
ogr2ogr -f GeoJSON masts.geojson area.osm -dialect sqlite \
  -sql "SELECT * FROM points WHERE other_tags LIKE '%\"man_made\"=>\"mast\"%'"

# reproject on the way out, so the areas and distances you measure are in metres
ogr2ogr -f GPKG -t_srs EPSG:32631 buildings_utm.gpkg area.gpkg buildings

Queries are metered by server CPU time, not request count: a careless [timeout:900] over a whole country will get you a 429 and then a temporary ban from the main instance. Run the count form first. Because OSM is crowd-sourced, a feature’s absence means nobody mapped it — never that it is not there — and tag completeness varies by an order of magnitude between countries. For proximity searches without writing Overpass QL by hand, Bellingcat’s OpenStreetMap Search wraps the same data.

GDAL

The library every GIS tool is built on, and faster than any of them from a shell. Install gdal with your package manager (brew install gdal, apt install gdal-bin). In OSINT work it does four things: tell you what a file actually contains, crop and reproject it, georeference a photograph or scanned map, and build band maths like NDVI without opening a GUI.

# what is in this file: CRS, extent, pixel size, bands, and the capture metadata
gdalinfo -stats scene.tif | head -40

# the same, machine-readable, when you are checking fifty files rather than reading one
gdalinfo -json scene.tif | jq '{crs: .coordinateSystem.wkt[0:60], size, bands: [.bands[].band]}'

# crop to a bounding box in the file's own CRS, so you stop moving 2GB around
gdal_translate -projwin 4.88 52.38 4.90 52.37 -projwin_srs EPSG:4326 scene.tif crop.tif

# the same crop by pixel window, when you are working off a screenshot's coordinates
gdal_translate -srcwin 2400 1850 512 512 scene.tif tile.tif

# reproject to Web Mercator for overlay on a slippy basemap
gdalwarp -t_srs EPSG:3857 -r cubic crop.tif crop_3857.tif

# clip to an area of interest polygon rather than a rectangle, and trim the canvas to it
gdalwarp -cutline aoi.gpkg -crop_to_cutline -dstalpha -overwrite scene.tif aoi_only.tif

# georeference a scanned map: four ground control points (pixel x, pixel y, lon, lat), then warp
gdal_translate -of GTiff -a_srs EPSG:4326 \
  -gcp 120 95 4.8855 52.3805 -gcp 1890 110 4.9015 52.3799 \
  -gcp 1875 1410 4.9010 52.3702 -gcp 135 1395 4.8860 52.3708 \
  scan.png scan_gcp.tif
gdalwarp -r cubic -t_srs EPSG:4326 -overwrite scan_gcp.tif scan_geo.tif

# mosaic a directory of tiles into one virtual raster — no copying, instant
gdalbuildvrt mosaic.vrt tiles/*.tif

# NDVI from Sentinel-2 bands: vegetation loss, burn scars, new earthworks
gdal_calc.py -A B08.jp2 -B B04.jp2 --outfile=ndvi.tif \
  --calc="(A.astype(float)-B)/(A.astype(float)+B+0.0001)"

# differencing two dates: --extent=intersect is what stops a silent misalignment
gdal_calc.py -A ndvi_may.tif -B ndvi_sep.tif --outfile=ndvi_delta.tif \
  --calc="B-A" --type=Float32 --extent=intersect --projectionCheck --overwrite

# hillshade from a DEM, for reading terrain in a photograph's background
gdaldem hillshade -z 2 dem.tif hillshade.tif

# multidirectional hillshade keeps slopes facing away from the light readable
gdaldem hillshade -multidirectional -compute_edges dem.tif hillshade_multi.tif

# slope in degrees, for arguing about whether a vehicle track is plausible
gdaldem slope -compute_edges dem.tif slope.tif

# a shareable PNG at a sane size, with the world file so it stays georeferenced
gdal_translate -of PNG -outsize 25% 25% -co WORLDFILE=YES crop.tif preview.png

The one command that settles arguments is gdallocationinfo: it reads the pixel value at a coordinate, which turns “that looks darker” into a number you can put in a report.

# the band values under one WGS84 coordinate, values only, coordinate echoed back
echo "4.8909 52.3738" | gdallocationinfo -wgs84 -valonly -E -field_sep , scene.tif

# a whole candidate list in one pass: stdin is read line by line
gdallocationinfo -wgs84 -valonly -E -field_sep , ndvi_delta.tif < candidates.txt

# the elevation under a coordinate, which is how you check a claimed camera height
echo "4.8909 52.3738" | gdallocationinfo -wgs84 -valonly dem.tif

gdalinfo is the honesty check: if it reports no CRS, the file is a picture and any measurement you take off it is invented. Georeferencing error concentrates away from your control points, so put them at the corners of the area you care about and expect metres of error, not centimetres. Resampling with -r cubic makes imagery look better and makes pixel-level forensics worse — use -r near when the pixels themselves are the evidence. And gdal_calc.py will happily difference two rasters of different extents unless you say --extent=intersect, producing a delta image whose bright edges are registration error rather than change.

QGIS

The desktop GIS, and where a question stops being “look at this” and becomes “measure this, against these layers”. Free from qgis.org. Add satellite imagery as a basemap with Layer → Add Layer → Add XYZ Layer and one of these URLs:

https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}
https://tile.openstreetmap.org/{z}/{x}/{y}.png
https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}

Note the {z}/{y}/{x} order on the Esri service and {z}/{x}/{y} on the others — swapped axes are the reason a basemap loads as noise. The Georeferencer (Layer → Georeferencer) does the same job as the gdal_translate -gcp run above with a point-and-click interface and a visible residual error per point, which is worth the GUI on its own.

Everything in Processing also runs headless, which is how a one-off analysis becomes repeatable:

# every algorithm available, including the ones your installed plugins add
qgis_process list

# the parameters for one algorithm, before you guess at them
qgis_process help native:buffer

# machine-readable, for building a pipeline against the real parameter names
qgis_process --json help native:extractbylocation | jq '.parameters | keys'

# a 200m buffer around candidate points
qgis_process run native:buffer -- INPUT=candidates.gpkg DISTANCE=200 OUTPUT=buffered.gpkg

# reproject a layer to a metric CRS so distances mean something
qgis_process run native:reprojectlayer -- \
  INPUT=candidates.geojson TARGET_CRS='EPSG:3857' OUTPUT=candidates_3857.gpkg

# centroids of building polygons, for matching against a geotagged photo set
qgis_process run native:centroids -- INPUT=buildings.gpkg OUTPUT=centroids.gpkg

# keep only the features inside an area of interest. PREDICATE is an enum, not a word:
# 0 intersect, 1 contain, 2 disjoint, 3 equal, 4 touch, 5 overlap, 6 are within, 7 cross
qgis_process run native:extractbylocation -- \
  INPUT=centroids.gpkg PREDICATE=0 INTERSECT=aoi.gpkg OUTPUT=inside.gpkg

# units are a run-time choice, so a buffer in metres has to say so
qgis_process run native:buffer --distance_units=meters --area_units=m2 \
  --ellipsoid=EPSG:7030 -- INPUT=candidates.gpkg DISTANCE=200 OUTPUT=buffered.gpkg

# against a project, so layer references and saved styles resolve
qgis_process run native:centroids --project_path=case.qgz -- \
  INPUT=buildings.gpkg OUTPUT=centroids.gpkg

# parameters as JSON on stdin, which is how this goes into a script without quoting pain
echo '{"inputs": {"INPUT": "candidates.gpkg", "DISTANCE": 200, "OUTPUT": "buffered.gpkg"}}' \
  | qgis_process run native:buffer -

# startup is dominated by plugin loading; skip it for a batch of a hundred runs
qgis_process --no-python --skip-loading-plugins run native:centroids -- \
  INPUT=buildings.gpkg OUTPUT=centroids.gpkg

Measurements in a geographic CRS (EPSG:4326) are in degrees, not metres, and QGIS will happily give you a meaningless number. Reproject to a local metric CRS or a UTM zone before you measure anything you intend to publish, and say which CRS you used. --skip-loading-plugins is not free of consequence either: an algorithm provided by a plugin disappears from list when you pass it, and the failure reads as a missing algorithm rather than a missing plugin.

Copernicus Data Space

The free Sentinel archive, and the only no-cost source with a revisit frequency short enough for change detection. Sentinel-2 gives 10m optical every five days; Sentinel-1 is radar and sees through cloud. The catalogue is searchable without an account; downloading needs a free registration. Note that Sentinel Hub Playground and the old EO Browser are retired — the browser is now Copernicus Browser.

CAT='https://catalogue.dataspace.copernicus.eu/odata/v1/Products'

# what Sentinel-2 exists for a date window — no token needed for search
curl -s -G "$CAT" \
  --data-urlencode "\$filter=Collection/Name eq 'SENTINEL-2' and ContentDate/Start gt 2026-09-01T00:00:00.000Z and ContentDate/Start lt 2026-09-05T00:00:00.000Z" \
  --data-urlencode '$top=5' | jq -r '.value[] | "\(.ContentDate.Start)  \(.Name)"'

# the same, bounded to an area of interest
curl -s -G "$CAT" \
  --data-urlencode "\$filter=Collection/Name eq 'SENTINEL-2' and OData.CSC.Intersects(area=geography'SRID=4326;POLYGON((4.85 52.36,4.95 52.36,4.95 52.40,4.85 52.40,4.85 52.36))') and ContentDate/Start gt 2026-08-01T00:00:00.000Z" \
  --data-urlencode '$top=10' | jq -r '.value[].Name'

# radar instead, for a cloudy week
curl -s -G "$CAT" \
  --data-urlencode "\$filter=Collection/Name eq 'SENTINEL-1' and ContentDate/Start gt 2026-09-01T00:00:00.000Z" \
  --data-urlencode '$top=5' | jq -r '.value[].Name'

# a download needs a token from the Keycloak identity service
export ACCESS_TOKEN=$(curl -s -d 'client_id=cdse-public' -d "username=$CDSE_USER" \
  -d "password=$CDSE_PASS" -d 'grant_type=password' \
  'https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token' \
  | jq -r .access_token)

# then fetch the product by its Id
curl -s -L -H "Authorization: Bearer $ACCESS_TOKEN" \
  "$CAT(08f7cbba-56c6-4730-b2d1-63ee8a5b9536)/\$value" -o product.zip

Three refinements turn that from a listing into a search. contains(Name,'MSIL2A') restricts to the atmospherically corrected processing level, which is the only level you may compare between dates. The attribute form filters on cloud cover, which is the single number that decides whether a scene is worth downloading. And $count=True reports the total so you learn how many scenes exist before you page through them.

POLY="POLYGON((4.885 52.370,4.897 52.370,4.897 52.378,4.885 52.378,4.885 52.370))"

# L2A only, over the polygon, under 10 per cent cloud, oldest first, with a total count.
# The attribute syntax is verbose and exact: the value type appears twice.
curl -s -G "$CAT" \
  --data-urlencode "\$filter=Collection/Name eq 'SENTINEL-2' and contains(Name,'MSIL2A') and OData.CSC.Intersects(area=geography'SRID=4326;$POLY') and ContentDate/Start gt 2026-05-01T00:00:00.000Z and ContentDate/Start lt 2026-06-01T00:00:00.000Z and Attributes/OData.CSC.DoubleAttribute/any(att:att/Name eq 'cloudCover' and att/OData.CSC.DoubleAttribute/Value lt 10.00)" \
  --data-urlencode '$orderby=ContentDate/Start asc' \
  --data-urlencode '$count=True' --data-urlencode '$top=3' \
  | jq -r '"count: \(.["@odata.count"])", (.value[] | "\(.ContentDate.Start[0:19])  \(.Name)")'

# paging: the response carries @odata.nextLink, already signed with every parameter.
# Follow it rather than incrementing $skip by hand.
curl -s -G "$CAT" \
  --data-urlencode "\$filter=Collection/Name eq 'SENTINEL-1' and ContentDate/Start gt 2026-09-01T00:00:00.000Z" \
  --data-urlencode '$count=True' --data-urlencode '$top=20' \
  | jq -r '.["@odata.nextLink"]'

# the Id, which is the only thing the download endpoint accepts
curl -s -G "$CAT" \
  --data-urlencode "\$filter=contains(Name,'S2A_MSIL2A_20260501T104651')" \
  | jq -r '.value[] | "\(.Id)  \(.ContentLength)  \(.Online)"'

Filter on Collection/Name or the query will be slow enough to time out. A Sentinel-2 product name encodes the tile and the processing level — L2A is atmospherically corrected and what you want for comparing two dates; L1C is not. The cloud percentage in the metadata is for the whole 100km tile, so a “12% cloud” scene can still be solid cloud over your target, and the inverse also bites: a 60 per cent scene can be perfectly clear over your 500m box. Filter on it to rank, never to exclude outright. Online: false means the product has been moved to cold storage and the download will stall rather than fail. Tokens expire in minutes; re-request rather than caching them.

Google Earth Pro

Web and desktop, free, and still the most accessible archive of sub-metre historical imagery. The desktop build is the one worth having, because the browser version hides the two features that matter.

The historical slider (View → Historical Imagery, or the clock icon) steps through every coverage date for the view. Work it deliberately: note the date stamp in the status bar for every frame you use, because that stamp is the whole evidentiary value of the screenshot. The elevation profile (draw a path, then Edit → Show Elevation Profile) answers “could you see X from Y”, which is a common verification question that imagery alone cannot settle.

Ruler tool            line and path lengths, plus a bearing in degrees
Polygon + area        rooftop and compound areas, for matching a described structure
Show Elevation Profile line-of-sight and terrain between two points
Sun / lighting slider  shadow direction at a chosen date and time
Import KML/GPX         drop in Overpass output or a track for overlay
Status-bar date stamp  the capture date of the imagery currently drawn

There is no command line, so the scriptable surface is KML, which Earth Pro reads and writes. Two documents are worth keeping as templates. A Placemark with a LookAt reproduces an exact view — position, bearing and camera distance — so a colleague opens the same frame rather than the same coordinate. A GroundOverlay drapes a georeferenced scan or a cropped Sentinel tile over the imagery at a stated extent, which is how you compare your own raster against Earth Pro’s archive without leaving Earth Pro.

<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
  <Document>
    <!-- reproduces a view, not just a point: heading is the bearing the camera
         faces, tilt 0 is straight down, range is metres from the target -->
    <Placemark>
      <name>Warehouse, south elevation</name>
      <LookAt>
        <longitude>4.8909</longitude>
        <latitude>52.3738</latitude>
        <altitude>0</altitude>
        <heading>312</heading>
        <tilt>65</tilt>
        <range>400</range>
      </LookAt>
      <TimeSpan>
        <begin>2026-05-01</begin>
        <end>2026-09-30</end>
      </TimeSpan>
      <Point><coordinates>4.8909,52.3738,0</coordinates></Point>
    </Placemark>

    <!-- drape your own raster over Earth Pro's imagery. The LatLonBox must match
         the file's real extent: gdalinfo prints it, and guessing it shifts
         everything you then "measure" off the overlay -->
    <GroundOverlay>
      <name>Sentinel-2 L2A crop, 2026-05-01</name>
      <color>b4ffffff</color>
      <drawOrder>1</drawOrder>
      <Icon><href>crop_wgs84.png</href></Icon>
      <altitudeMode>clampToGround</altitudeMode>
      <LatLonBox>
        <north>52.378</north>
        <south>52.370</south>
        <east>4.897</east>
        <west>4.885</west>
        <rotation>0</rotation>
      </LatLonBox>
    </GroundOverlay>
  </Document>
</kml>
# produce the overlay PNG the KML above expects: WGS84, so the LatLonBox is honest
gdalwarp -t_srs EPSG:4326 -r near crop.tif crop_wgs84.tif
gdal_translate -of PNG crop_wgs84.tif crop_wgs84.png

# read the extent back out, in the order the LatLonBox wants it
gdalinfo -json crop_wgs84.tif \
  | jq -r '.wgs84Extent.coordinates[0] | "west \(.[0][0])  south \(.[0][1])  east \(.[2][0])  north \(.[2][1])"'

Sharing a view outside Earth Pro has a documented URL form as well, which is the fastest way to hand someone a Street View frame at a specific bearing rather than a coordinate they then have to orient themselves in.

https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=52.3738,4.8909&heading=312&pitch=0&fov=80
https://www.google.com/maps/@?api=1&map_action=map&center=52.3738,4.8909&zoom=18&basemap=satellite

  api=1         required, and the URL silently misbehaves without it
  viewpoint     lat,lon -- Google snaps to the nearest panorama, which may be the wrong street
  pano          a specific panorama id, when you need that exact capture and not the nearest
  heading       -180 to 360, compass bearing the camera faces
  pitch         -90 to 90, 0 is horizontal
  fov           10 to 100, default 90 -- narrowing it is the closest thing to a zoom
  zoom          0 to 21 on map_action=map
  basemap       roadmap, satellite or terrain

The displayed date is the date of the dominant image in the view; a mosaic can blend captures months apart, with a visible seam. Zooming changes which image is drawn, so the date can change under you without the view appearing to move. 3D buildings are models, not imagery, and are not evidence of anything. A viewpoint link resolves to whatever panorama is nearest at the time someone opens it, so for anything you intend to cite, capture the pano id instead — the nearest panorama changes when Google drives the street again.

Google Earth Engine

The free analysis platform for the same archives, and the right tool when the question spans more scenes than you want to download. The code editor runs JavaScript server-side against the full Sentinel and Landsat catalogues; a free account is needed and non-commercial use is free. The pattern is always the same: a geometry, a collection, filters, then a composite or a difference.

// an area of interest, not a point: filterBounds takes any geometry
var aoi = ee.Geometry.Rectangle([4.885, 52.370, 4.897, 52.378]);

// the harmonised collection, because the pre-2022 and post-2022 scenes are
// otherwise offset by a processing baseline change and every difference is wrong
var s2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
  .filterBounds(aoi)
  .filterDate('2026-05-01', '2026-06-01')
  .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20));

print('scenes matched', s2.size());

// the least cloudy scene in the window, rather than the first one returned
var best = ee.Image(s2.sort('CLOUDY_PIXEL_PERCENTAGE').first());
print('chosen', best.get('system:index'), best.get('CLOUDY_PIXEL_PERCENTAGE'));

Map.setCenter(4.891, 52.374, 16);
Map.addLayer(best, {bands: ['B4', 'B3', 'B2'], min: 0, max: 3000}, 'true colour');

// false colour: vegetation bright red, bare ground and new works pale.
// This is the composite that makes a demolition or an earthwork obvious at 10m.
Map.addLayer(best, {bands: ['B8', 'B4', 'B3'], min: 0, max: 4000}, 'false colour');

// a two-date NDVI difference, which is change detection without downloading anything
var ndvi = function (img) {
  return img.normalizedDifference(['B8', 'B4']).rename('ndvi');
};
var may = ndvi(ee.Image(s2.sort('CLOUDY_PIXEL_PERCENTAGE').first()));
var sep = ndvi(ee.Image(
  ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
    .filterBounds(aoi)
    .filterDate('2026-09-01', '2026-10-01')
    .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 40))
    .sort('CLOUDY_PIXEL_PERCENTAGE')
    .first()));

Map.addLayer(sep.subtract(may).clip(aoi),
  {min: -0.6, max: 0.6, palette: ['red', 'white', 'green']}, 'NDVI delta');

The thing to distrust here is the compositing. A median over a date range looks clean and is not an image of any moment — it is a statistic, and it cannot carry a capture date, so it supports no time-sensitive claim. Pick a single scene and name it when the claim is about a date. CLOUDY_PIXEL_PERCENTAGE is the whole-tile figure again, so a 15 per cent scene can still be clouded over your box; look at the picture before you trust the number. And Map.addLayer stretches reflectance to a min/max you chose, so two dates displayed with different stretches will look different whether or not anything changed.

Mapillary

Crowd-sourced street-level imagery, frequently covering roads Google never drove and often more recent. Free API, and unlike Street View it is queryable by bounding box and capture date, which makes “what did this junction look like in March” a scriptable question.

# token from mapillary.com/dashboard/developers — the prefix is literally "MLY|"
TOKEN='MLY|xxxx|xxxx'
API='https://graph.mapillary.com'

# images in a bounding box (left,bottom,right,top) — must be under 0.01 degrees square
curl -s -H "Authorization: OAuth $TOKEN" \
  "$API/images?bbox=4.890,52.372,4.895,52.375&fields=id,captured_at,compass_angle,geometry&limit=50" \
  | jq -r '.data[] | "\(.captured_at)  \(.id)"'

# only images from a date window, which is the point of using this over Street View
curl -s -H "Authorization: OAuth $TOKEN" \
  "$API/images?bbox=4.890,52.372,4.895,52.375&start_captured_at=2026-03-01T00:00:00Z&end_captured_at=2026-04-01T00:00:00Z&fields=id,captured_at"

# one image in full, including the direction the camera faced and the original file
curl -s -H "Authorization: OAuth $TOKEN" \
  "$API/IMAGE_ID?fields=id,captured_at,compass_angle,camera_type,thumb_2048_url,geometry"

# the detected objects in a frame — traffic signs are the usable ones for geolocation
curl -s -H "Authorization: OAuth $TOKEN" \
  "$API/images?bbox=4.890,52.372,4.895,52.375&fields=id,detections.value"

# download the frame you are going to cite
curl -s -H "Authorization: OAuth $TOKEN" "$API/IMAGE_ID?fields=thumb_2048_url" \
  | jq -r .thumb_2048_url | xargs curl -s -o frame.jpg

Four more parameters are worth knowing. is_pano separates 360-degree captures, which are the ones where you can look behind the camera; creator_username and organization_id let you decide how much to trust a sequence by who uploaded it; and sequence_ids plus the image_ids endpoint walks a single drive in capture order, which is what turns a set of frames into a direction of travel.

# 360 captures only, with the computed fields alongside the raw ones
curl -s -H "Authorization: OAuth $TOKEN" \
  "$API/images?bbox=4.890,52.372,4.895,52.375&is_pano=true&fields=id,captured_at,is_pano,compass_angle,computed_compass_angle,computed_geometry&limit=100" \
  | jq -r '.data[] | "\(.captured_at)  raw \(.compass_angle)  computed \(.computed_compass_angle)"'

# who uploaded what here: trust a municipal fleet differently from one contributor
curl -s -H "Authorization: OAuth $TOKEN" \
  "$API/images?bbox=4.890,52.372,4.895,52.375&fields=id,captured_at,creator,organization" \
  | jq -r '.data[] | "\(.captured_at)  \(.creator.username // "-")  \(.organization // "-")"'

# every image id in one sequence, in capture order. This endpoint ignores `fields`.
curl -s -H "Authorization: OAuth $TOKEN" "$API/image_ids?sequence_id=SEQUENCE_ID" \
  | jq -r '.data[].id' | head -20

# the sequence a frame belongs to, then the frames either side of it
curl -s -H "Authorization: OAuth $TOKEN" "$API/IMAGE_ID?fields=sequence,captured_at"

# detections on one frame as a separate edge, with geometry per detection
curl -s -H "Authorization: OAuth $TOKEN" \
  "$API/IMAGE_ID/detections?fields=value,geometry,created_at" \
  | jq -r '.data[] | .value' | sort | uniq -c | sort -rn

The bounding box limit is real: anything larger than 0.01 degrees square is rejected rather than truncated, so tile your area. limit defaults to 2000 and tops out there, so a dense city tile can be truncated without an error — compare the count you get against the count you expected rather than assuming you have everything. compass_angle is the camera bearing and is what lets you say which side of the street a feature is on; computed_compass_angle and computed_geometry are the structure-from-motion refinements and are usually the better of the two, but they exist only for frames that were successfully reconstructed, so a null there is a frame whose position is raw GPS. Coverage is contributor-driven, so a road can have 2019 and 2026 imagery and nothing between, and sequence positions are GPS traces — metres of error in cities, more in canyons.

EarthExplorer and the LandsatLook STAC API

Web only, free with registration, and the only practical route to two archives nothing else carries: Landsat back to 1972, and declassified US reconnaissance imagery from the 1960s to the 1980s at resolutions that still surprise people.

Search at earthexplorer.usgs.gov. The interface is four tabs and the order matters:

1 Search Criteria   draw a polygon or enter coordinates, then set the date range
2 Data Sets         Landsat → Landsat Collection 2 Level-2 for analysis-ready scenes
                    Declassified Data → Declass 1/2/3 for CORONA, ARGON, KH-7 and KH-9
3 Additional Criteria  cloud cover ceiling, and the sensor for declassified missions
4 Results           the footprint icon shows coverage; the browse icon previews it

For the Landsat half of that, there is a scriptable route that needs no account at all. USGS runs a STAC API at landsatlook.usgs.gov/stac-server, and search is open. This is the fastest way to find out whether a usable Landsat scene exists over a point in a window, and the response carries the sun azimuth and elevation per scene, which is the shadow geometry you need for chronolocation without computing anything.

STAC='https://landsatlook.usgs.gov/stac-server'

# what collections exist, and their ids. landsat-c2l2-sr is Level-2 surface reflectance.
curl -s "$STAC/collections" | jq -r '.collections[] | "\(.id)  \(.title)"'

# a search: bbox, date window, cloud ceiling, oldest first
curl -s -X POST "$STAC/search" -H 'Content-Type: application/json' -d '{
  "collections": ["landsat-c2l2-sr"],
  "bbox": [4.885, 52.370, 4.897, 52.378],
  "datetime": "2026-05-01T00:00:00Z/2026-09-30T23:59:59Z",
  "query": {"eo:cloud_cover": {"lt": 20}},
  "sortby": [{"field": "properties.datetime", "direction": "asc"}],
  "limit": 5
}' | jq -r '"matched: \(.numberMatched)",
  (.features[] | "\(.properties.datetime[0:19])  \(.id)  cloud \(.properties["eo:cloud_cover"])  sun_az \(.properties["view:sun_azimuth"])  sun_el \(.properties["view:sun_elevation"])")'

# the asset list for one scene: per-band COGs, addressable without downloading the scene
curl -s "$STAC/collections/landsat-c2l2-sr/items/LC09_L2SP_199023_20260528_20260530_02_T1_SR" \
  | jq -r '.assets | to_entries[] | "\(.key)  \(.value.href)"' | head -20

The same API from a shell, via pystac-client, which handles paging and saves a searchable item collection. --matched asks only for the count, which is the cheap question to ask first.

pipx install pystac-client    # or: pip install pystac-client

# how many scenes exist, before fetching any of them
stac-client search "$STAC" -c landsat-c2l2-sr \
  --bbox 4.885 52.370 4.897 52.378 --datetime 2026-05-01/2026-09-30 --matched

# the same search, cloud-filtered, newest first, saved to a file
stac-client search "$STAC" -c landsat-c2l2-sr \
  --bbox 4.885 52.370 4.897 52.378 --datetime 2026-05-01/2026-09-30 \
  --query "eo:cloud_cover<20" --sortby "-properties.datetime" \
  --max-items 20 --save landsat_items.json

# only the fields you need, piped straight into a table
stac-client search "$STAC" -c landsat-c2l2-sr \
  --bbox 4.885 52.370 4.897 52.378 --datetime 2026-07-01/2026-07-31 \
  --fields "id,properties.datetime,properties.eo:cloud_cover" \
  | jq -r '.features[] | [.id, .properties.datetime, .properties["eo:cloud_cover"]] | @tsv'

# an arbitrary polygon instead of a box: a GeoJSON file or an inline geometry
stac-client search "$STAC" -c landsat-c2l2-sr --intersects aoi.geojson \
  --datetime 2026-01-01/2026-12-31 --matched

Register before you search EarthExplorer, because the download buttons are hidden until you log in, and read the scene’s entity ID — it encodes the mission and date, and it is what you cite. Declassified frames arrive as scanned film with no georeferencing at all, which is where the gdal_translate -gcp workflow above earns its keep. Landsat Collection 2 Level-2 is corrected and comparable between dates; Level-1 is not, so do not difference the two. The STAC route covers only Landsat: the declassified archive is not in it, so that half of the job stays in the web interface.

NASA FIRMS

Active-fire detections from MODIS and VIIRS, as a CSV you can query by box and date. This is the only source on this page that answers “was there a fire or an explosion here, and when” to the hour, because the thermal sensors pass several times a day where the optical archive manages one cloud-free frame a week. A free map key from the FIRMS site is the only requirement.

KEY='your_map_key'
FIRMS='https://firms.modaps.eosdis.nasa.gov/api'

# which dates each product actually covers, before you ask for one that does not exist
curl -s "$FIRMS/data_availability/csv/$KEY/ALL"

# detections in an area over the last 3 days. AREA is west,south,east,north --
# the opposite order to Overpass, and the usual source of an empty result set
curl -s "$FIRMS/area/csv/$KEY/VIIRS_SNPP_NRT/34.0,31.0,36.5,33.5/3"

# a historical window: same path plus a start date, YYYY-MM-DD. Day range is 1 to 5,
# so a month is six requests, not one.
curl -s "$FIRMS/area/csv/$KEY/VIIRS_NOAA20_NRT/34.0,31.0,36.5,33.5/5/2026-09-01"

# the whole globe for a day, when you do not yet know where to look
curl -s "$FIRMS/area/csv/$KEY/MODIS_NRT/world/1"

# high-confidence night-time detections only, sorted by brightness
curl -s "$FIRMS/area/csv/$KEY/VIIRS_SNPP_NRT/34.0,31.0,36.5,33.5/3" \
  | awk -F, 'NR==1 || ($14=="h" && $12=="N")' | sort -t, -k3 -rn | head

Read the columns before you read the map. confidence is l/n/h for VIIRS and a percentage for MODIS, daynight is D or N, frp is fire radiative power in megawatts, and scan/track give the pixel footprint — which is 375m for VIIRS and 1km for MODIS, so a detection is an area, not a point, and plotting it as a dot overstates your precision by hundreds of metres. The _NRT sources are near-real-time and get reprocessed; the _SP standard-product versions are the ones to cite weeks later, and they will not agree exactly. A detection is a thermal anomaly: gas flares, industrial furnaces and sunglint off metal roofs all produce them, so the question a FIRMS hit answers is “was something hot here at 22:14 UTC”, not “was there an airstrike”. Conflict-specific use of this feed is on Conflict & Environment.

NASA Worldview and GIBS

Worldview is the browser (worldview.earthdata.nasa.gov) and GIBS is the tile service behind it. The resolution is coarse — 250m at best — and that is not the point: GIBS serves a dated, global, cloud-free-ish image for every single day back years, with no key and no account, which no other source on this page does. It is how you establish what the weather was doing on the day your high-resolution scene is missing.

GIBS='https://gibs.earthdata.nasa.gov'

# one WMTS tile, REST form:
#   /wmts/{projection}/best/{layer}/default/{time}/{tilematrixset}/{z}/{row}/{col}.{ext}
curl -s -o tile.jpg \
  "$GIBS/wmts/epsg4326/best/VIIRS_SNPP_CorrectedReflectance_TrueColor/default/2026-09-20/250m/6/13/36.jpg"

# a bounded image via WMS instead, which is what you want for an area of interest.
# Version 1.3.0 uses CRS and, for EPSG:4326, BBOX in lat,lon order: south,west,north,east
curl -s -o scene.png "$GIBS/wms/epsg4326/best/wms.cgi?\
version=1.3.0&service=WMS&request=GetMap&format=image/png&STYLE=default\
&CRS=EPSG:4326&BBOX=52.3,4.8,52.5,5.0&WIDTH=1200&HEIGHT=1200&TIME=2026-09-20\
&LAYERS=VIIRS_SNPP_CorrectedReflectance_TrueColor"

# the layer catalogue, including each layer's available date range
curl -s "$GIBS/wmts/epsg4326/best/1.0.0/WMTSCapabilities.xml" \
  | grep -oE '<ows:Identifier>[^<]+' | sed 's/.*>//' | head -40

The WMS endpoint is a GDAL data source, so the day-by-day archive can be pulled straight into the same pipeline as everything else, which beats screenshotting the browser:

# GDAL reads the WMS URL directly, so crop and reproject in one step
gdal_translate -of GTiff -projwin 4.8 52.5 5.0 52.3 -projwin_srs EPSG:4326 \
  "WMS:$GIBS/wms/epsg4326/best/wms.cgi?LAYERS=VIIRS_SNPP_CorrectedReflectance_TrueColor&TIME=2026-09-20&SRS=EPSG:4326&FORMAT=image/png&VERSION=1.1.1" \
  day.tif

# a week of daily frames, to find the cloud-free day worth paying attention to
for d in 2026-09-{14..20}; do
  curl -s -o "gibs_$d.png" "$GIBS/wms/epsg4326/best/wms.cgi?\
version=1.3.0&service=WMS&request=GetMap&format=image/png&STYLE=default\
&CRS=EPSG:4326&BBOX=52.0,4.0,53.0,5.5&WIDTH=800&HEIGHT=800&TIME=$d\
&LAYERS=VIIRS_SNPP_CorrectedReflectance_TrueColor"
done

Projections are separate endpoints — epsg4326, epsg3857, epsg3413 and epsg3031 — and a layer present in one is not necessarily present in another, with the polar projections carrying far fewer. WMS 1.3.0 reverses the BBOX axis order for EPSG:4326 relative to 1.1.1, which is the reason a request returns ocean when you asked for land; if the image looks like the wrong hemisphere, swap the pairs rather than doubting the coordinates. TIME is a date, not a timestamp, and the frame you get is the composite for that day’s overpasses, so a “2026-09-20” image spans hours. At 250m a building is a fifth of a pixel: use this to date weather and smoke plumes, never to identify a structure.

SunCalc and ShadeMap

Web only, free, and the fastest way to put a time on a photograph you have already geolocated. A shadow’s direction and length at a known place is a clock, and these two read it in opposite directions.

SunCalc takes a coordinate and a date and draws the sun’s azimuth and elevation through the day; you match the shadow bearing in the image to the time that produces it. ShadeMap does the inverse, simulating the shadows that buildings and terrain actually cast at a chosen moment, which is what you need in a city where the shadow comes off a tower rather than the subject.

Input:   coordinate, date, and the shadow bearing measured off the image
Read:    the time or times of day whose azimuth matches that bearing
Check:   shadow LENGTH against solar elevation — bearing alone gives two candidate times
Capture: the coordinate, date, computed azimuth and elevation, and the tool's own URL
Caveat:  a date you have not independently established makes the whole result circular

SunCalc keeps its entire state in the URL fragment, which is the only part of it worth treating as a command line. The form is #/<lat>,<lon>,<zoom>/<YYYY.MM.DD>/<HH:MM>/<object height>, so a link reproduces a specific reading rather than just opening the tool:

https://www.suncalc.org/#/52.3738,4.8909,17/2026.07.14/17:40/5/2

  52.3738,4.8909  the coordinate
  17              map zoom
  2026.07.14      date, dot-separated
  17:40           local time at that coordinate, in the timezone the page reports
  5               object height in metres, which drives the shadow-length readout

Both assume you have the location right; a 50m error in position barely moves the azimuth, but a wrong date moves it by degrees per week near the solstices. Shadow work narrows a time, it does not prove one — pair it with image and video forensics and with whatever the metadata claims. The full arithmetic, the two-date-window problem and the Python libraries that sweep a whole year are on Geolocation & Chronolocation.

Tool reference

ToolWhat it doesCost
Apple MapsApple Maps is a digital mapping service with detailed maps, satellite imagery, and location-based information.free
Baidu MapsBaidu’s mapping service offering satellite imagery, street maps, and streetview (“Panorama” - zh:百度全景).free
Bellingcat OpenStreetMap SearchA user interface to search OpenStreetMap data for features in proximity to each other.free
Bing MapsBing Maps is a web mapping service provided by Microsoft that offers detailed geographical information and tools for location search, and satellite…partly free
Carte.maMapping/streetview service for Moroccofree
Convert Geographic UnitsA tool that converts various geographic coordinates to support diverse mapping and spatial analysis needs.free
Copernicus Browser (formerly Sentinel Hub Playground, EO Browser)A free web-based platform for viewing, analyzing, and downloading satellite imagery from the European Space Agency’s Sentinel missions, with data updated…free
EarthExplorerEarthExplorer is an archive portal from the U.S. Geological Survey (USGS) that allows users search a location and time range to discover and access…partly free
EOS LandviewerEOS Landviewer provides free services for up to 10 images. More images and analysis are available to journalists at a discount. Contact: Artem Seredyuk…paid
F4MapF4Map is an interactive 3D map visualization tool that provides detailed rendering of urban landscapes and geographical features.free
Gaode MapsGaode Maps (also known as AMap) is a mapping application and technology from the Chinese company Alibaba.free
GeoHintsGeoHints is a website that provides information about things like traffic lights, utility poles, bollards etc. for different regions of the world to help…free
GjirafaMapping service for Albania (specially Kosovo)free
Global Forest WatchExplore tree cover loss and gain data, recent deforestation and fire alerts, land use designations, carbon emissions, biodiversity metrics and more.free
Google Earth EngineGoogle Earth Engine is a platform for environmental monitoring, land use change and object/infrastructure detection through satellite imagery and…free
Google Earth ProGoogle Earth is a geospatial tool that provides detailed, global satellite imagery, maps, 3D terrain models, and the ability to explore geographic data…partly free
Google MapsGoogle Maps provides mapping information, satellite imagery and Google Street View imagery including historical Street View images.free
GovMapGovMap provides an interactive map of Israel, offering users a wide range of data including property boundaries, planning information, and infrastructure…free
HERE WeGoMapping service similar to Google Maps or Apple Maps.free
Hitta.seMapping service for Swedenfree
Index DatabaseA database which relates remote sensing indices with satellite imaging sensorsfree
Kakao MapA mapping application provided by South Korean technology company Kakao Corp.free
KartaViewKartaView is a crowdsourced platform for street view imagery.free
Mapa.skMapping service for Slovakiafree
MapCheckingThis tool helps you estimate and fact-check the maximum number of people standing in a given area.free
MapillaryMapillary is a crowdsourced street-level imagery platform.free
MappyMapping service (and streetview in a couple of French cities [double check this!])free
MapSwitcherChrome extension switches between online map apps, maintaining (as far as possible) the map centre, zoom level, & directions of the source map.free
mapy.czMapping service for Czechiafree
Maritime Awareness ProjectSouth China Sea maps with oil and gas fields, fishing areas, air defense zones and administrative, claimed, disputed zones, submarine data cables.free
NASA FIRMSDisplays a world map overlaid with infra-red data from one or more satellites, some, but not all of which may represent heat from fires and explosions.free
NASA WorldviewNASA Worldview is an online tool for visualizing and downloading near real-time satellite imagery and scientific data of Earth’s atmosphere, land, and…free
OpenAerialMapPlatform for accessing open-licensed satellite and unmanned aerial vehicle (UAV) imageryfree
OpenInfraMapPower lines, telecoms, solar, oil, gas & water infrastructure mapped globally.free
OpenSeaMapSea map of borders, special zones, shipping lanes, with overlays of MarineTraffic and other sourcesfree
OpenStreetMapOpenStreetMap is a collaborative project to create a free editable map of the world.free
OrbTrackPredicts & describes the position & path of >15,000 satellites in Earth orbit, relative to points on the earth’s surface input by the user, for 5 days…free
Overpass TurboOverpass Turbo is a web-based tool for querying and visualizing OpenStreetMap crowd sourced data, aiding in extracting specific information like locations…free
PeakVisorDual window views for any global location: (1) a 2-D map & (2) a 3-D rendered terrain model, with photo fitting, shade/slope mapping, sun trails & weather…free
Photo-Map.RUGeotagged VK posts.free
Planet LabsPlanet Labs PBC is an American optical satellite imagery company that sells access to imagery.partly free
QGISQGIS is a free Open Source Geographic Information System (GIS).free
Quick geolocation searchA tool that brings several maps into one place for easy location search.free
Radar Interference Tracker (RIT)Bellingcat’s radar interference tracker can be used to locate and monitor active military radar systems.free
RAMMB SLIDERReal-time weather satellites of the entire globefree
Satellites.proSatellites.pro allows open source researchers to quickly switch between several free satellite imagery and mapping services.free
ShadeMapShadeMap is a global simulation of mountain, building & tree shadows for a given date & time. Base data is free, but users can buy 30cm accurate data per…partly free
ShadowMapGlobal map of 3D buildlings and the shadows they cast at a specific time a dayfree
SkyFiSkyFi is used to purchase commercial satellite imagery and task (order the collection of images) satellites without a subscription.paid
StravaA fitness tracking platform where publicly shared GPS activity data can reveal movement patterns, routines, and precise locations of individuals…partly free
Tencent MapsTencent Maps (formerly SOSO Maps) is a desktop and web mapping service application and technology provided by Chinese company Tencent, offering satellite…free
The European Space Agency (ESA) - Earth OnlineThe ESA’s Earth Online product offers a portal for accessing satellite imagery and environmental data, supporting a range of applications from climate…free
Topotijdreis.nlOver 200 years of maps and topography from the Netherlands.free
Umbra SpaceUmbra is an American synthetic aperture radar (SAR) satellite imaging company that sells on-demand taskings for satellite imagery.paid
UTM grid zonesAn overview of the Universal Transverse Mercator coordinate system.free
what3wordsA proprietary geocode system which identifies any location on the surface of the earth to a resolution of 3 metres. The identifier is a unique combination…partly free
WikimapiaWikimapia is a long-running collaborative mapping project that remains partially accessible, providing open source researchers with a unique database of…free
Yandex MapsA platform offering detailed maps, satellite imagery, street views (static & sometimes dynamic imagery, including aerial views). Often the best available…partly free
About Maps and SatellitesA guide to using map and satellite tools.free

Pitfalls

  • Undated imagery is useless for a time-sensitive claim. Record the capture date every time.
  • Cloud cover ruins optical revisit rates. A 5-day nominal revisit can mean a month of usable imagery in the wet season. Radar (Sentinel-1) sees through cloud but is much harder to read.
  • A scene’s cloud percentage is for the whole tile, not your target. It is a 100km tile for Sentinel-2 and a 185km swath for Landsat. Use the number to rank candidates and then look at the picture; a 60 per cent scene can be clear over your box and a 12 per cent scene can be solid cloud over it.
  • OSM is crowd-sourced. Completeness varies enormously by region, and an absent feature may simply be unmapped.
  • Basemap labels disagree, particularly on disputed borders and place names. Say which source you used.
  • Bounding boxes are in four different orders across these tools. Overpass takes south,west,north,east; Mapillary and STAC take left,bottom,right,top; FIRMS takes west,south,east,north; and WMS 1.3.0 flips to lat,lon for EPSG:4326 where 1.1.1 does not. An empty result set is this mistake far more often than it is an absence of data.
  • A median composite has no capture date. It is a statistic over a window, so it cannot support a claim about a day. Pick a single scene and name it.
  • Processing levels are not comparable. L2A against L1C, or Landsat Level-2 against Level-1, produces a difference image of the atmospheric correction rather than of the ground.
  • Differencing two rasters of different extents aligns them silently. gdal_calc.py without --extent=intersect will give you a delta whose brightest features are registration error.
  • A 10m pixel cannot resolve a 10m object. Two or three pixels across is the floor for seeing that something is there; you need an order of magnitude better to say what it is. A roof six pixels wide is enough to see it disappear and not enough to see how.
  • Street-level coverage is a sample, not a survey. A junction with 2019 and 2026 frames and nothing between does not mean nothing happened in between, and the newest frame is not the frame nearest your date.
  • A thermal detection is an area, not a point. VIIRS pixels are 375m and MODIS 1km, so plotting a FIRMS hit as a dot claims a precision the sensor does not have.
  • Near-real-time products get reprocessed. A FIRMS _NRT detection and the later _SP version of the same pass will not agree exactly, so cite the one you can still retrieve.
  • Google’s historical slider date is the dominant image’s date. A mosaic blends captures months apart, and changing zoom can change which image is drawn without the view appearing to move.

Worked example

One datum: the coordinate 52.3738, 4.8909, pulled from a photograph’s caption, and a claim that a warehouse there was demolished in the summer of 2026.

  1. Establish what is mapped. An Overpass query for building ways in a small box around the coordinate returns three polygons, one tagged building=warehouse with an addr:street. That gives the feature a name and an address to search on. The (newer:"2026-06-01T00:00:00Z") form on the same query shows one of the three edited in August 2026, which is a free first corroboration of the claim’s timing from OSM’s own history.
  2. Find free imagery either side of the claim. The Copernicus OData catalogue, filtered to SENTINEL-2, contains(Name,'MSIL2A') and intersected with a 1.3km polygon around the point. Run once per month with a 10 per cent cloud ceiling, and once without, because the gap between those two answers is the real story:
=== MAY 2026, cloudCover < 10 ===
count: 3
  2026-05-01T10:36:19  S2B_MSIL2A_20260501T103619_N0512_R008_T31UFU_20260501T143617.SAFE
  2026-05-01T10:46:51  S2A_MSIL2A_20260501T104651_N0512_R051_T31UFU_20260501T173800.SAFE
  2026-05-26T10:36:21  S2C_MSIL2A_20260526T103621_N0512_R008_T31UFU_20260526T140311.SAFE

=== SEPTEMBER 2026, cloudCover < 10 ===
count: 0

=== SEPTEMBER 2026, cloudCover < 40 ===
count: 2
  2026-09-01T10:46:19  S2B_MSIL2A_20260901T104619_N0512_R051_T31UFU_20260901T131914.SAFE
  2026-09-25T10:40:41  S2A_MSIL2A_20260925T104041_N0513_R008_T31UFU_20260925T171206.SAFE

=== SEPTEMBER 2026, no cloud filter ===
count: 18

Eighteen scenes in the month, two under 40 per cent cloud, none under 10. The nominal five-day revisit is an orbital fact; the usable cadence over the Netherlands in September is a fortnight. Take the 25 September scene and accept that it needs looking at rather than trusting. 3. Crop and compare. gdal_translate -projwin cuts both scenes to the same 500m box, gdalwarp -t_srs EPSG:3857 puts them in the same CRS, and the pair opened in QGIS shows the roof present on 1 May and bare ground on 25 September. At 10m the roof is six pixels across — enough to see it go, not enough to see how. gdallocationinfo -wgs84 -valonly on the NDVI difference at the coordinate returns +0.02, confirming what the eye says: this is roof giving way to bare ground, not vegetation change. 4. Bracket it with Landsat, for the sun geometry. The LandsatLook STAC search over the same box needs no account and returns, for May to September with a 20 per cent cloud ceiling:

matched: 12
2026-05-28T10:38:56  LC09_L2SP_199023_20260528_20260530_02_T1_SR  cloud 4.51   sun_az 153.53  sun_el 56.26
2026-06-22T10:33:16  LC09_L2SP_198024_20260622_20260623_02_T1_SR  cloud 7.52   sun_az 148.59  sun_el 58.83
2026-06-29T10:39:07  LC09_L2SP_199023_20260629_20260630_02_T1_SR  cloud 6.09   sun_az 150.22  sun_el 57.48
2026-07-15T10:39:14  LC09_L2SP_199023_20260715_20260717_02_T1_SR  cloud 0.22   sun_az 150.28  sun_el 55.66

30m is too coarse to see the building, so this is not the change-detection source here. What it gives you is view:sun_elevation around 55.7 degrees at 10:39 UTC in mid-July, which is the number to check the photograph’s shadows against in step 7. 5. Confirm at resolution. Google Earth Pro’s historical slider over the same point has a July 2026 frame at sub-metre scale showing partial demolition and plant on site. Record the status-bar date, not the date you looked, and capture the view as a LookAt placemark so the frame is reproducible rather than described. 6. Check the ground. Mapillary images?bbox=4.8900,52.3730,4.8915,52.3745&start_captured_at=2026-08-01T00:00:00Z returns a contributor sequence from August with computed_compass_angle 312 degrees, facing the plot: hoarding up, structure gone. Street level dates the end of the work more precisely than any satellite pass. The sequence runs north-west along the street, so the frames either side establish that the hoarding is on the plot boundary and not on the one next door. 7. Measure, then say so. Reprojected to UTM zone 31N (EPSG:32631), the QGIS measure tool puts the cleared footprint at 1,840 m², against 1,795 m² for the OSM polygon — a 2.5 per cent disagreement, which is within what a 10m pixel edge can produce and is therefore consistent rather than confirming. Quote the CRS alongside the number. 8. Time the photograph, if it matters. The caption claims mid-July. SunCalc for the coordinate on 14 July, with the lamp standard’s 5m height entered, returns an azimuth matching the shadow bearing at around 17:40 local and a solar elevation of about 30 degrees — consistent with the Landsat-derived geometry for the same week, and recorded as consistent rather than proven.

What you can assert: a structure present on a named, dated 10m scene on 1 May 2026 and absent from a named, dated 10m scene on 25 September 2026; a sub-metre Google Earth Pro frame stamped July 2026 showing demolition in progress; a Mapillary sequence from August 2026 showing the site hoarded and cleared; and a cleared footprint of 1,840 m² in EPSG:32631. The demolition therefore falls between 1 May and 25 September, and the July frame narrows it to the first half of that window. What you cannot assert: who did it, or why — no imagery source on this page carries that.

What would falsify it: a July Google Earth Pro frame that turns out to be a mosaic blending a pre-demolition capture from a neighbouring strip, which the visible seam would show and the status-bar date would not; a Mapillary sequence whose computed_geometry is absent, leaving the position as raw GPS and the “facing the plot” claim unsupported; or a warehouse that was rebuilt and re-demolished, which two dated frames five months apart cannot distinguish from one event. The measurement carrying the most risk is the 1,840 m² footprint. It is traced off 10m pixels, so each edge carries at best half a pixel of uncertainty: on a roughly 43m square that is about ±5m per side, or ±8 per cent on the area. Quote it as 1,840 m² ±150 m² or do not quote a figure at all — and note that this tolerance is why the 2.5 per cent agreement with the OSM polygon in step 7 corroborates nothing. Two numbers that agree inside their error bars are not a cross-check.

Broader catalogues

More tools

Further tools for this area from the OSINT Newsletter Tools Library (Geolocation and Maps OSINT), excluding those already listed above.

ToolWhat it does
EarthPoint ConvertA web-based conversion tool for transforming geographic coordinate data between formats.
GeoconfirmedA collaborative OSINT platform to help analysts verify and geolocate images, videos, and events from conflicts worldwide.
GeoSpyAn AI-powered geolocation tool that analyses images to estimate where they were taken by examining visual features like…
MoonCalcA free web-based lunar positioning tool that visualises the position, phase and illumination of the moon for any location, date…
MW GeofindFinds geotagged YouTube videos on a map.
N2YO Satellite TrackerReal-time satellite tracking platform providing orbital data, pass predictions and positional information for thousands of…
Open Infrastructure MapAn interactive mapping platform that visualises critical infrastructure worldwide using data primarily sourced from OpenStreetMap.
PicartaAI-powered geolocation tool that finds where a photo was taken using visual analysis.
SPOTAI-powered geolocation tool to help identify where an image was taken by analysing visual elements including landmarks, terrain…
SunCalcA free web-based geolocation tool that visualises the position of the sun and shadows for any location, date, and time.
Surveillance under SurveillanceAn interactive map that visualises CCTV and surveillance camera locations worldwide using OpenStreetMap data.

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.

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.

On this page

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.