pygeospy ๐
Python GEOINT/OSINT library with a Rust-accelerated core. Given any image, coordinates, IP, or set of clues โ produce a location.
Install
pip install pygeospy
Prebuilt wheels are published for Linux (x86-64), macOS (Apple Silicon), and Windows (x86-64) on CPython 3.10+. On those platforms the Rust core is included and no toolchain is needed. Anywhere else, pip builds from the source distribution, which requires a Rust toolchain; if the build is skipped or fails, the library still imports and runs on its pure-Python fallbacks (see Rust core).
Optional heavy dependencies (vision, OCR, audio, etc.) live behind extras:
pip install "pygeospy[all]" # everything
pip install "pygeospy[coords,exif]" # pick modules
Verify the install โ and whether the Rust core is active:
pygeospy info
# pygeospy v0.2.1
# Rust core (_rustcore): โ available
Run this from outside a checkout of this repository. From the repo root the local
pygeospy/source directory shadows the installed package, and you will see the pure-Python fallback instead of the wheel you just installed.
What makes pygeospy different?
| Feature | pygeospy | Other OSINT tools |
|---|---|---|
| Rust core | โ 10โ100ร faster batch math | Pure Python only |
| SAR module | โ NASAR grids + ISRID profiles | Not available |
| Full pipeline | โ analyze(anything) โ coordinates |
Module-only APIs |
| Acoustic analysis | โ BirdNET + siren classification | Not available |
| Offline-first | โ LLaVA/Ollama, zero API keys needed | Cloud-dependent |
Quick Start
import pygeospy
# Haversine distance (Rust-accelerated)
dist = pygeospy.coords.haversine(51.5, -0.1, 48.85, 2.35)
print(f"London โ Paris: {dist:.1f} km")
# Shadow โ latitude band
result = pygeospy.solar.latitude_band_from_shadow(
shadow_ratio=2.5, # shadow is 2.5ร taller than object
shadow_azimuth_deg=195, # shadow points south-southwest
)
print(f"Candidate latitude bands: {result.candidate_lat_bands}")
print(f"Season: {result.estimated_season}")
# EXIF extraction
exif = pygeospy.exif.extract("photo.jpg")
if exif.has_gps:
print(f"GPS: {exif.coordinates}")
# Full pipeline analysis
result = pygeospy.pipeline.analyze(
"mystery_photo.jpg",
shadow_ratio=2.5,
shadow_azimuth_deg=195,
vision_backend="llava", # offline, no API key needed
export=True, # saves HTML report, GeoJSON, KML, GPX
)
print(result.summary)
Modules
v0.1 โ Foundation
| Module | Description | Backend |
|---|---|---|
pygeospy.coords |
Haversine, bearing, UTM, MGRS, bounding boxes, elevation API | Rust + Python |
pygeospy.solar |
Shadow โ sun angle โ latitude bands, sunrise/sunset | Rust + Python |
pygeospy.exif |
GPS, camera fingerprinting, forensic scrub detection, batch | Python |
pygeospy.terrain |
Slope, aspect, TRI, viewshed, elevation profile | Rust + Python |
pygeospy.osm |
Overpass queries, building footprints, road density | Python |
pygeospy.geo |
Nominatim geocoding, reverse geo, IP geolocation | Python |
pygeospy.sar |
NASAR grid, corridors, POA zones, urgency scoring | Rust + Python |
pygeospy.export |
Folium maps, HTML reports, GeoJSON/KML/GPX | Python |
v0.2 โ Visual Intelligence
| Module | Description | Backend |
|---|---|---|
pygeospy.visual |
Infrastructure/sign/vegetation/vehicle clues, Claude/GPT-4V/LLaVA | Python |
pygeospy.chronos |
Shadow โ time of day, vegetation โ season, weather archives | Python |
pygeospy.language |
OCR, script detection (18 systems), sign geocoding | Python |
pygeospy.network |
IP/ASN, WiGLE BSSID, MAC OUI, email headers, crt.sh | Python |
pygeospy.satellite |
Sentinel-2 search, NDVI/EVI/MNDWI, change detection | Rust + Python |
pygeospy.acoustic |
BirdNET species โ region, siren tones, Whisper language | Python |
pygeospy.pipeline |
Unified analyze() engine, parallel execution |
Python |
CLI
# Full analysis
pygeospy analyze mystery_photo.jpg --shadow-ratio 2.5 --shadow-azimuth 195 --export
# Solar position
pygeospy solar position 51.5 -0.1 172 14.0
# Shadow โ latitude bands
pygeospy solar from-shadow 2.5 195 --doy 172
# EXIF extraction
pygeospy exif extract photo.jpg
# Coordinate conversion
pygeospy coords convert 48.8566 2.3522 --fmt all
# Haversine
pygeospy coords haversine 51.5 -0.1 48.85 2.35
# SAR grid
pygeospy sar grid --lat 47.6 --lon -122.3 --radius 3.0 --cell 0.5 --out grid.geojson
# SAR urgency
pygeospy sar urgency --age 8 --medical --hours 6 --night
# IP analysis
pygeospy analyze --ip 8.8.8.8
# Cache management
pygeospy cache stats
pygeospy cache clear
Architecture
pygeospy/
โโโ _rustcore/ # Rust crate (PyO3, abi3)
โ โโโ src/
โ โโโ lib.rs # Module entry point
โ โโโ coords.rs # Haversine, bearing, UTM, bbox
โ โโโ solar.rs # Solar elevation/azimuth, shadow geometry
โ โโโ terrain.rs # Slope, aspect, TRI, viewshed
โ โโโ sar.rs # Grid generation, POA rings, urgency
โ โโโ raster.rs # NDVI, EVI, pixel statistics, Otsu
โโโ pygeospy/ # Python package
โ โโโ __init__.py
โ โโโ _types.py # GeoResult, Clue, LatLon, BoundingBox
โ โโโ _utils.py # Shared utilities, rate limiter
โ โโโ _cache.py # Disk cache with TTL
โ โโโ coords.py # Rust wrapper + elevation/timezone APIs
โ โโโ solar.py # Rust wrapper + GeoJSON export
โ โโโ exif.py # EXIF extraction and forensics
โ โโโ terrain.py # Rust wrapper + DEM download
โ โโโ osm.py # Overpass API queries
โ โโโ geo.py # Nominatim + IP lookup
โ โโโ sar.py # Rust wrapper + GPX export
โ โโโ export.py # Folium maps, HTML reports
โ โโโ visual.py # Vision model integration
โ โโโ chronos.py # Temporal analysis
โ โโโ language.py # OCR + linguistic analysis
โ โโโ network.py # IP/network OSINT
โ โโโ satellite.py # Sentinel-2 + spectral indices
โ โโโ acoustic.py # Audio geographic signals
โ โโโ pipeline.py # Unified analysis engine
โ โโโ cli.py # Typer CLI
โโโ tests/
โ โโโ test_coords.py
โ โโโ test_solar.py
โ โโโ test_sar.py
โ โโโ test_terrain.py
โ โโโ test_pipeline.py
โโโ scripts/
โ โโโ check_encoding.py # CI guard: no NUL bytes / valid UTF-8
โ โโโ release.py # bump changelog + versions together
โโโ .github/workflows/
โ โโโ ci.yml # tests (3 OS x 3 Python), Rust build, lint, changelog
โ โโโ release.yml # wheels + sdist -> PyPI (OIDC) -> GitHub Release
โโโ pyproject.toml
โโโ CHANGELOG.md # Keep a Changelog, validated by patchnotes
โโโ Makefile
โโโ README.md
The compiled extension is installed as pygeospy._rustcore (inside the package),
not as a top-level module โ so the _rustcore/ crate directory in the repo root
cannot shadow it.
Example: Brick-Wall-to-Coordinates Pipeline
The classic GEOINT workflow, automated:
import pygeospy
# Step 1: Check EXIF
exif = pygeospy.exif.extract("brick_wall.jpg")
# โ No GPS found, EXIF timestamp: 2024-06-15 14:23:00
# Step 2: Solar analysis from shadow
solar = pygeospy.solar.analyze_shadow(
shadow_ratio=2.1, # measured from image
shadow_azimuth_deg=200, # estimated from image
timestamp_utc="2024-06-15T14:23:00Z",
)
# โ Candidate bands: 35ยฐNโ55ยฐN (northern summer afternoon)
# Step 3: Visual clues (offline with LLaVA)
pygeospy.visual.set_backend("llava")
clues = pygeospy.visual.extract_clues("brick_wall.jpg")
# โ brick bond: English bond โ Northern Europe / UK
# โ mortar: white repointing โ post-1950 UK
# โ stone sill: grey limestone โ Northern England / Scotland
# Step 4: OSM region narrowing
from pygeospy._types import BoundingBox
bb = BoundingBox(50, -5, 58, 2) # England
arch = pygeospy.osm.architectural_tags(53.8, -1.5, radius_m=500)
# Step 5: Full pipeline
result = pygeospy.pipeline.analyze(
"brick_wall.jpg",
shadow_ratio=2.1,
shadow_azimuth_deg=200,
vision_backend="llava",
export=True,
)
print(result.summary)
print(result.candidate_countries[:3])
Building the Rust Core
The Rust core builds to a single abi3 extension โ pygeospy/_rustcore.*.so on
Linux/macOS, pygeospy/_rustcore.*.pyd on Windows โ that works across Python
3.10+ without recompiling per version.
If the extension is missing, every module falls back to pure Python
automatically (with a RuntimeWarning at import). Results are identical; only
the batch-heavy paths are slower. Nothing is unavailable without Rust.
# Prerequisites: a Rust toolchain (https://rustup.rs) and maturin
pip install maturin
# Development build โ installs into the active virtualenv.
# NOTE: maturin develop requires an ACTIVATED virtualenv; it will not install
# into a bare system Python.
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
maturin develop --release
# Or build a wheel and install it
maturin build --release -o dist
pip install dist/*.whl
Check which backend is live:
from pygeospy._utils import RUST_AVAILABLE
print(RUST_AVAILABLE) # True once the extension is built and importable
Vision Model Backends
import pygeospy.visual as v
# Claude (best accuracy, requires API key)
v.set_backend("claude", api_key="sk-ant-...")
# GPT-4V (requires OpenAI API key)
v.set_backend("gpt4v", api_key="sk-...")
# LLaVA via Ollama โ FULLY OFFLINE, no API key needed
# Install: https://ollama.ai then: ollama pull llava:13b
v.set_backend("llava", base_url="http://localhost:11434", model="llava:13b")
# Rule-based only (no model) โ this is the DEFAULT
v.set_backend("none")
What the backend actually determines. Visual clue extraction (brick bond,
signage, vegetation, vehicles) is performed by the vision model, not by pygeospy.
With "none" โ the default โ visual.extract_clues() returns little or nothing,
and pipeline runs on an image with no EXIF GPS will find few clues. The
brick-wall example below is the plumbing working end to end with a model
attached; the inference quality is the model's, and the clue-to-country mapping
is a coarse keyword table, not a trained geolocator. Treat candidate countries as
a ranked hypothesis to investigate, not an answer.
Optional API Keys
| Service | Module | Required? | Notes |
|---|---|---|---|
| Anthropic Claude | visual |
Optional | Best visual analysis |
| OpenAI GPT-4V | visual |
Optional | Alternative |
| ip-api.com | geo, network |
Optional | Free tier: 45 req/min |
| WiGLE | network |
Optional | Wi-Fi BSSID lookup |
| What3Words | geo |
Optional | W3W address conversion |
| Meteostat | chronos |
Optional | Historical weather |
| Open-Topo-Data | coords, terrain |
Free / no key | Elevation data |
All core features work without any API keys.
Testing
pip install pytest
PYTHONPATH=. pytest tests/ -v
71 tests, no network required. They exercise the pure-Python paths by default; CI additionally runs the whole suite against a built Rust core on Linux, macOS, and Windows.
Other checks CI runs
pip install ruff patchnotes
ruff check pygeospy/ tests/ # lint
python scripts/check_encoding.py # no NUL bytes / valid UTF-8 in sources
patchnotes CHANGELOG.md validate --strict
scripts/check_encoding.py exists because a stray run of NUL bytes appended to
_rustcore/src/sar.rs once made cargo reject the file outright, silently
disabling the Rust core for months while the pure-Python fallback covered for it.
Roadmap
- Web UI (FastAPI + Leaflet)
pygeospy.crowdโ crowd-sourced Wikidata location signalspygeospy.timelineโ multi-image temporal reconstruction- QGIS plugin
- Wheels for Linux aarch64 and macOS x86-64
Contributing
Changes are tracked in CHANGELOG.md, which follows
Keep a Changelog and is validated in CI with
patchnotes. Add your entry under
## [Unreleased] using one of the standard sections (Added, Changed,
Deprecated, Removed, Fixed, Security) โ a non-standard heading or a
non-ISO date fails the build with an annotation on the offending line.
On release, python scripts/release.py <version> moves the [Unreleased] block
into a dated release and syncs the version across pyproject.toml and
pygeospy/__init__.py (CI fails if those drift). The tag's changelog entry then
becomes the GitHub Release body automatically.
License
MIT ยฉ pygeospy contributors
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pygeospy-0.2.2.tar.gz.
File metadata
- Download URL: pygeospy-0.2.2.tar.gz
- Upload date:
- Size: 81.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
31b7ea7d29d0282f22f9ea740ecf0e63a2301f74b639c788aa51cc841ba73332
|
|
| MD5 |
aad8ac6aaee8179f6bf73f4413ffd14d
|
|
| BLAKE2b-256 |
61464f4a5fbbc5c90c9c842fc416a7f0733825e1f23cd82e742d26856425200d
|
Provenance
The following attestation bundles were made for pygeospy-0.2.2.tar.gz:
Publisher:
release.yml on Londopy/pygeospy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pygeospy-0.2.2.tar.gz -
Subject digest:
31b7ea7d29d0282f22f9ea740ecf0e63a2301f74b639c788aa51cc841ba73332 - Sigstore transparency entry: 2318980628
- Sigstore integration time:
-
Permalink:
Londopy/pygeospy@4b53f5e78c2b3346e13c7eb666f091a7f824d23a -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/Londopy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4b53f5e78c2b3346e13c7eb666f091a7f824d23a -
Trigger Event:
push
-
Statement type:
File details
Details for the file pygeospy-0.2.2-cp310-abi3-win_amd64.whl.
File metadata
- Download URL: pygeospy-0.2.2-cp310-abi3-win_amd64.whl
- Upload date:
- Size: 241.7 kB
- Tags: CPython 3.10+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
532182d79affa8f836fba94ede1bfb691b3e9f06cd4bf12501d64d73e94e4710
|
|
| MD5 |
5996cf8acb3afd1b6edc10a5f280c9a5
|
|
| BLAKE2b-256 |
809482be0314aa661bbc1ad4d8c7fd4bda2572be995f384b9a23ca251b4dd8bf
|
Provenance
The following attestation bundles were made for pygeospy-0.2.2-cp310-abi3-win_amd64.whl:
Publisher:
release.yml on Londopy/pygeospy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pygeospy-0.2.2-cp310-abi3-win_amd64.whl -
Subject digest:
532182d79affa8f836fba94ede1bfb691b3e9f06cd4bf12501d64d73e94e4710 - Sigstore transparency entry: 2318981213
- Sigstore integration time:
-
Permalink:
Londopy/pygeospy@4b53f5e78c2b3346e13c7eb666f091a7f824d23a -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/Londopy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4b53f5e78c2b3346e13c7eb666f091a7f824d23a -
Trigger Event:
push
-
Statement type:
File details
Details for the file pygeospy-0.2.2-cp310-abi3-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: pygeospy-0.2.2-cp310-abi3-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 326.5 kB
- Tags: CPython 3.10+, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e2c8f1929c2f1dee832c6192a1522f0569c227dd0bba245ba20a546ea325ecc5
|
|
| MD5 |
58432bc149f8786caf37cdaae1cfc37a
|
|
| BLAKE2b-256 |
059746e0bc40812baecc7caef27125c5f90d3b2a34614cf622edaca3a7c8e066
|
Provenance
The following attestation bundles were made for pygeospy-0.2.2-cp310-abi3-manylinux_2_34_x86_64.whl:
Publisher:
release.yml on Londopy/pygeospy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pygeospy-0.2.2-cp310-abi3-manylinux_2_34_x86_64.whl -
Subject digest:
e2c8f1929c2f1dee832c6192a1522f0569c227dd0bba245ba20a546ea325ecc5 - Sigstore transparency entry: 2318981506
- Sigstore integration time:
-
Permalink:
Londopy/pygeospy@4b53f5e78c2b3346e13c7eb666f091a7f824d23a -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/Londopy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4b53f5e78c2b3346e13c7eb666f091a7f824d23a -
Trigger Event:
push
-
Statement type:
File details
Details for the file pygeospy-0.2.2-cp310-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: pygeospy-0.2.2-cp310-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 304.5 kB
- Tags: CPython 3.10+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a0e9970a7a770c487cf835369f2146bd021bb6e0e99268d9614cc1a306108521
|
|
| MD5 |
5971eeae704f041d3b84cbde5cc69b72
|
|
| BLAKE2b-256 |
5505aa3df56eb9c83a73f6c5862b9e1c1c9a8de56a0df1d66a36ad2bdb54abbf
|
Provenance
The following attestation bundles were made for pygeospy-0.2.2-cp310-abi3-macosx_11_0_arm64.whl:
Publisher:
release.yml on Londopy/pygeospy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pygeospy-0.2.2-cp310-abi3-macosx_11_0_arm64.whl -
Subject digest:
a0e9970a7a770c487cf835369f2146bd021bb6e0e99268d9614cc1a306108521 - Sigstore transparency entry: 2318980899
- Sigstore integration time:
-
Permalink:
Londopy/pygeospy@4b53f5e78c2b3346e13c7eb666f091a7f824d23a -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/Londopy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4b53f5e78c2b3346e13c7eb666f091a7f824d23a -
Trigger Event:
push
-
Statement type: