Skip to main content

pygeospy ๐ŸŒ

Python GEOINT/OSINT library with a Rust-accelerated core. Given any image, coordinates, IP, or set of clues โ€” produce a location.

PyPI CI Python 3.10+ Platforms License: MIT Rust


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 signals
  • pygeospy.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

pygeospy-0.2.2.tar.gz (81.3 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

pygeospy-0.2.2-cp310-abi3-win_amd64.whl (241.7 kB view details)

Uploaded CPython 3.10+Windows x86-64

pygeospy-0.2.2-cp310-abi3-manylinux_2_34_x86_64.whl (326.5 kB view details)

Uploaded CPython 3.10+manylinux: glibc 2.34+ x86-64

pygeospy-0.2.2-cp310-abi3-macosx_11_0_arm64.whl (304.5 kB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

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

Hashes for pygeospy-0.2.2.tar.gz
Algorithm Hash digest
SHA256 31b7ea7d29d0282f22f9ea740ecf0e63a2301f74b639c788aa51cc841ba73332
MD5 aad8ac6aaee8179f6bf73f4413ffd14d
BLAKE2b-256 61464f4a5fbbc5c90c9c842fc416a7f0733825e1f23cd82e742d26856425200d

See more details on using hashes here.

Provenance

The following attestation bundles were made for pygeospy-0.2.2.tar.gz:

Publisher: release.yml on Londopy/pygeospy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for pygeospy-0.2.2-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 532182d79affa8f836fba94ede1bfb691b3e9f06cd4bf12501d64d73e94e4710
MD5 5996cf8acb3afd1b6edc10a5f280c9a5
BLAKE2b-256 809482be0314aa661bbc1ad4d8c7fd4bda2572be995f384b9a23ca251b4dd8bf

See more details on using hashes here.

Provenance

The following attestation bundles were made for pygeospy-0.2.2-cp310-abi3-win_amd64.whl:

Publisher: release.yml on Londopy/pygeospy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pygeospy-0.2.2-cp310-abi3-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for pygeospy-0.2.2-cp310-abi3-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 e2c8f1929c2f1dee832c6192a1522f0569c227dd0bba245ba20a546ea325ecc5
MD5 58432bc149f8786caf37cdaae1cfc37a
BLAKE2b-256 059746e0bc40812baecc7caef27125c5f90d3b2a34614cf622edaca3a7c8e066

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pygeospy-0.2.2-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for pygeospy-0.2.2-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 a0e9970a7a770c487cf835369f2146bd021bb6e0e99268d9614cc1a306108521
MD5 5971eeae704f041d3b84cbde5cc69b72
BLAKE2b-256 5505aa3df56eb9c83a73f6c5862b9e1c1c9a8de56a0df1d66a36ad2bdb54abbf

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page