geosint 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geosint-1.0.0/LICENSE +21 -0
- geosint-1.0.0/PKG-INFO +436 -0
- geosint-1.0.0/README.md +401 -0
- geosint-1.0.0/geosint/__init__.py +8 -0
- geosint-1.0.0/geosint/__main__.py +6 -0
- geosint-1.0.0/geosint/analysis.py +314 -0
- geosint-1.0.0/geosint/api_client.py +202 -0
- geosint-1.0.0/geosint/batch.py +69 -0
- geosint-1.0.0/geosint/bulk_extract.py +131 -0
- geosint-1.0.0/geosint/cache.py +92 -0
- geosint-1.0.0/geosint/cli.py +1115 -0
- geosint-1.0.0/geosint/config.py +235 -0
- geosint-1.0.0/geosint/db.py +970 -0
- geosint-1.0.0/geosint/dedupe.py +110 -0
- geosint-1.0.0/geosint/display.py +347 -0
- geosint-1.0.0/geosint/entities.py +303 -0
- geosint-1.0.0/geosint/exceptions.py +17 -0
- geosint-1.0.0/geosint/exporters.py +127 -0
- geosint-1.0.0/geosint/geo.py +147 -0
- geosint-1.0.0/geosint/osm_client.py +491 -0
- geosint-1.0.0/geosint/useragent.py +37 -0
- geosint-1.0.0/geosint/website_health.py +170 -0
- geosint-1.0.0/geosint/wizard.py +211 -0
- geosint-1.0.0/geosint.egg-info/PKG-INFO +436 -0
- geosint-1.0.0/geosint.egg-info/SOURCES.txt +46 -0
- geosint-1.0.0/geosint.egg-info/dependency_links.txt +1 -0
- geosint-1.0.0/geosint.egg-info/entry_points.txt +2 -0
- geosint-1.0.0/geosint.egg-info/requires.txt +14 -0
- geosint-1.0.0/geosint.egg-info/top_level.txt +1 -0
- geosint-1.0.0/pyproject.toml +48 -0
- geosint-1.0.0/setup.cfg +4 -0
- geosint-1.0.0/tests/test_analysis.py +376 -0
- geosint-1.0.0/tests/test_api_client.py +118 -0
- geosint-1.0.0/tests/test_batch.py +77 -0
- geosint-1.0.0/tests/test_bulk_extract.py +71 -0
- geosint-1.0.0/tests/test_cache.py +81 -0
- geosint-1.0.0/tests/test_cli.py +345 -0
- geosint-1.0.0/tests/test_colocation_grid.py +213 -0
- geosint-1.0.0/tests/test_db.py +369 -0
- geosint-1.0.0/tests/test_dedupe.py +179 -0
- geosint-1.0.0/tests/test_display.py +213 -0
- geosint-1.0.0/tests/test_entities.py +185 -0
- geosint-1.0.0/tests/test_exporters.py +119 -0
- geosint-1.0.0/tests/test_geo.py +137 -0
- geosint-1.0.0/tests/test_osm_client.py +138 -0
- geosint-1.0.0/tests/test_osm_client_hardening.py +275 -0
- geosint-1.0.0/tests/test_regressions.py +1010 -0
- geosint-1.0.0/tests/test_website_health.py +200 -0
geosint-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jenil Mistry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
geosint-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: geosint
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Geospatial OSINT collection and monitoring for points of interest and organisations - OpenStreetMap by default, optionally Google Places.
|
|
5
|
+
Author: Jenil Mistry
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jenilmistryhq/geosint
|
|
8
|
+
Project-URL: Repository, https://github.com/jenilmistryhq/geosint
|
|
9
|
+
Project-URL: Issues, https://github.com/jenilmistryhq/geosint/issues
|
|
10
|
+
Keywords: osint,geospatial,openstreetmap,overpass,geojson,reconnaissance,monitoring,investigation
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Information Technology
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Classifier: Environment :: Console
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: requests>=2.31
|
|
25
|
+
Provides-Extra: xlsx
|
|
26
|
+
Requires-Dist: openpyxl>=3.1; extra == "xlsx"
|
|
27
|
+
Provides-Extra: dotenv
|
|
28
|
+
Requires-Dist: python-dotenv>=1.0; extra == "dotenv"
|
|
29
|
+
Provides-Extra: bulk
|
|
30
|
+
Requires-Dist: osmium>=3.7; extra == "bulk"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
33
|
+
Requires-Dist: osmium>=3.7; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# geosint
|
|
37
|
+
|
|
38
|
+
Geospatial OSINT collection and monitoring for points of interest and
|
|
39
|
+
organisations. Collects from public map data, records **where every fact came
|
|
40
|
+
from**, reports **what changed between collections**, and correlates entities
|
|
41
|
+
by shared infrastructure.
|
|
42
|
+
|
|
43
|
+
[](https://pypi.org/project/geosint/)
|
|
44
|
+
[](LICENSE)
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
$ geosint collect --what "bureau de change" --area "Austin, TX" --case meridian
|
|
48
|
+
|
|
49
|
+
3 entities collected (0 new to this case)
|
|
50
|
+
|
|
51
|
+
ENTITY CATEGORY ADDRESS PHONE DOMAIN CONF TRIAGE
|
|
52
|
+
----------------- ---------------- --------- --------------- ----------------- ---- ----------
|
|
53
|
+
Alpha Exchange bureau_de_change 1 Main St +1 555 077 2200 newholdco.example 83 Unreviewed
|
|
54
|
+
Beta Money Ser... bureau_de_change 1 Main St +1 555 010 0100 holdco.example 83 Unreviewed
|
|
55
|
+
Cedar Currency bureau_de_change Cedar Ave +1 555 044 7788 67 Unreviewed
|
|
56
|
+
|
|
57
|
+
3/3 have usable coordinates.
|
|
58
|
+
|
|
59
|
+
3 field change(s) since the last collection:
|
|
60
|
+
|
|
61
|
+
WHEN ENTITY FIELD WAS NOW SOURCE
|
|
62
|
+
---------------- -------------- ------- ---------------------- --------------------- ------
|
|
63
|
+
2026-09-09 19:31 Alpha Exchange phone +1 555 010 0100 +1 555 077 2200 osm
|
|
64
|
+
2026-09-09 19:31 Alpha Exchange website https://holdco.example https://newholdco.e... osm
|
|
65
|
+
2026-09-09 19:31 Alpha Exchange domain holdco.example newholdco.example osm
|
|
66
|
+
|
|
67
|
+
1 previously-seen entity did not reappear:
|
|
68
|
+
- Northside Exchange
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Scope
|
|
72
|
+
|
|
73
|
+
This collects **organisations, businesses, and infrastructure** from public map
|
|
74
|
+
data — entities that publish an address and a phone number precisely so they
|
|
75
|
+
can be found. It queries each source's own official public API, at documented
|
|
76
|
+
rate limits, identifying itself with a contact address.
|
|
77
|
+
|
|
78
|
+
**It is not a people-search tool.** There is no person lookup, no
|
|
79
|
+
cross-referencing of individuals, and none should be added. Everything it
|
|
80
|
+
returns describes a *place* or an *organisation*.
|
|
81
|
+
|
|
82
|
+
## Start here
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install geosint
|
|
86
|
+
export GEOSINT_CONTACT="you@example.com" # required, see below
|
|
87
|
+
geosint start
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`start` asks five plain questions, remembers the answers, and runs your first
|
|
91
|
+
collection. Everything below is the same thing with flags.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Why this rather than a scraper
|
|
96
|
+
|
|
97
|
+
Three things, and they are the whole design.
|
|
98
|
+
|
|
99
|
+
**1. Provenance on every field.** Each record carries its source, source
|
|
100
|
+
reference, collection time, case, and the query that found it. `geosint show`
|
|
101
|
+
prints all of it. An answer you cannot attribute is not intelligence, it is
|
|
102
|
+
trivia.
|
|
103
|
+
|
|
104
|
+
**2. Change is the product.** Re-run a collection and the tool diffs it against
|
|
105
|
+
what is stored, writing an append-only observation for every field that moved.
|
|
106
|
+
A phone number changing, a website going dark, an entity vanishing from the
|
|
107
|
+
map — these are findings, and a tool that just overwrites its database destroys
|
|
108
|
+
them.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
geosint collect --what pharmacy --area "Austin, TX" --case alpha # week 1
|
|
112
|
+
geosint collect --what pharmacy --area "Austin, TX" --case alpha # week 2
|
|
113
|
+
geosint changes --since 7d --case alpha
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**3. Correlation between entities.** Everything else describes entities one at
|
|
117
|
+
a time. `geosint links` looks for what connects them:
|
|
118
|
+
|
|
119
|
+
| Link | Meaning |
|
|
120
|
+
|---|---|
|
|
121
|
+
| shared phone | Two "different" organisations answering one number |
|
|
122
|
+
| shared domain | Two entities publishing the same web host |
|
|
123
|
+
| operator / brand | Affiliation the source states outright — the strongest signal available |
|
|
124
|
+
| co-location | Entities within metres of each other |
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
$ geosint links --case meridian
|
|
128
|
+
|
|
129
|
+
Stated operator / brand affiliations
|
|
130
|
+
|
|
131
|
+
Affiliation: Meridian Holdings (2 entities)
|
|
132
|
+
- Alpha Exchange
|
|
133
|
+
- Beta Money Services
|
|
134
|
+
|
|
135
|
+
Co-located entities (within 25m)
|
|
136
|
+
|
|
137
|
+
Cluster: 2 entities within 25m (2 entities)
|
|
138
|
+
- Alpha Exchange
|
|
139
|
+
- Beta Money Services
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
A shared value is **a lead, not a conclusion**. Shared numbers and domains have
|
|
143
|
+
mundane explanations — a shared reception, a franchise call centre, a serviced
|
|
144
|
+
office — at least as often as interesting ones. The tool reports the evidence
|
|
145
|
+
and leaves the judgement to you.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Setup
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pip install geosint # core
|
|
153
|
+
pip install "geosint[xlsx,dotenv]" # + Excel export, .env support
|
|
154
|
+
pip install "geosint[bulk]" # + offline .osm.pbf extraction
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### The contact address is required
|
|
158
|
+
|
|
159
|
+
OpenStreetMap's Nominatim and Overpass are run by volunteers, and their usage
|
|
160
|
+
policy requires requests to carry a real contact so an operator can email you
|
|
161
|
+
rather than block you.
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
export GEOSINT_CONTACT="you@example.com" # macOS / Linux
|
|
165
|
+
$env:GEOSINT_CONTACT="you@example.com" # Windows PowerShell
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Or pass `--contact` per run. It is sent to OSM and to any site you explicitly
|
|
169
|
+
choose to check with `--check-websites`, and nowhere else.
|
|
170
|
+
|
|
171
|
+
### Optional: the Google collector
|
|
172
|
+
|
|
173
|
+
`--provider google` has better contact coverage but no `operator`, `brand`,
|
|
174
|
+
`ref`, or `wikidata` fields — the metadata that makes OSM more useful for
|
|
175
|
+
investigation. It also costs money past a free monthly allowance, and requires
|
|
176
|
+
billing enabled with a card on file even to stay inside it. Google prices a
|
|
177
|
+
whole response at the tier of its most expensive requested field, so
|
|
178
|
+
`--include-ratings` drops the free allowance from 5,000 to 1,000 calls/month.
|
|
179
|
+
|
|
180
|
+
1. https://console.cloud.google.com → new project
|
|
181
|
+
2. Enable **Places API (New)**, enable billing
|
|
182
|
+
3. Create an API key, restrict it to "Places API (New)"
|
|
183
|
+
4. `export GOOGLE_PLACES_API_KEY="your-key"`
|
|
184
|
+
|
|
185
|
+
`--near` / `--radius` become a Places `locationRestriction` rectangle around the
|
|
186
|
+
circle (the only shape Text Search accepts there), and results are then narrowed
|
|
187
|
+
to the true radius. The radius is capped at 50 km, since Text Search returns at
|
|
188
|
+
most 60 results - a wider radius is refused rather than quietly clamped. Use
|
|
189
|
+
`--provider osm` for a bigger sweep. `--tile` is osm-only.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Commands
|
|
194
|
+
|
|
195
|
+
### Collect
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
# A named area (geocoded to its bounding box)
|
|
199
|
+
geosint collect --what pharmacy --area "Austin, TX" --case alpha
|
|
200
|
+
|
|
201
|
+
# A point and radius - more precise, and usually what you want.
|
|
202
|
+
# A box around a city centre is mostly not the city centre.
|
|
203
|
+
geosint collect --what pharmacy --near 30.2672,-97.7431 --radius 2
|
|
204
|
+
|
|
205
|
+
# Any OSM tag, for anything not in the built-in category list
|
|
206
|
+
geosint collect --osm-tag "office=diplomatic" --area "Vienna, Austria"
|
|
207
|
+
geosint collect --osm-tag "man_made=mast" --near 51.5,-0.12 --radius 10
|
|
208
|
+
|
|
209
|
+
# Dense area under-returning? Split it into a 3x3 grid (9 queries, deduped)
|
|
210
|
+
geosint collect --what pharmacy --area "Chicago, IL" --tile 3
|
|
211
|
+
|
|
212
|
+
# Many collections from a CSV (header: what,area[,osm_tag]).
|
|
213
|
+
# Each row is its own tracked collection, so re-running the batch diffs
|
|
214
|
+
# every row and reports what stopped appearing, per row.
|
|
215
|
+
geosint batch --input targets.csv --case alpha
|
|
216
|
+
|
|
217
|
+
# A whole country or region, offline, no rate limit
|
|
218
|
+
geosint pbf --pbf texas-latest.osm.pbf --what pharmacy --case alpha
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
For `pbf`, download the smallest extract covering your region from
|
|
222
|
+
<https://download.geofabrik.de/>.
|
|
223
|
+
|
|
224
|
+
### Analyse
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
geosint entities --case alpha # browse
|
|
228
|
+
geosint entities --indicator shared_phone # filter by indicator
|
|
229
|
+
geosint entities --missing # what stopped appearing
|
|
230
|
+
geosint show "Alpha Exchange" # one entity + full provenance
|
|
231
|
+
geosint links --case alpha --tag # correlate, and tag the results
|
|
232
|
+
geosint near 30.2672,-97.7431 2 # what's within 2km
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Record judgement
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
geosint triage "Alpha Exchange" Flagged --note "same address as Beta"
|
|
239
|
+
geosint note "Alpha Exchange" "registration MSB-11021 predates the website"
|
|
240
|
+
geosint recheck "Alpha Exchange" +7d # also +2w, +3m, +1y, 2026-10-01
|
|
241
|
+
geosint recheck # what's due
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Triage states: `Unreviewed`, `Reviewing`, `Corroborated`, `Dismissed`,
|
|
245
|
+
`Flagged`. Anything you write — triage, notes, recheck dates, analyst — is
|
|
246
|
+
**never overwritten by a re-collection**.
|
|
247
|
+
|
|
248
|
+
### Monitor and report
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
geosint changes --since 7d --case alpha # what moved
|
|
252
|
+
geosint runs # collection history
|
|
253
|
+
geosint report --case alpha # indicator + link summary
|
|
254
|
+
geosint cases # all cases
|
|
255
|
+
geosint export --case alpha # GeoJSON by default
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`export` writes **GeoJSON** by default because the data is inherently
|
|
259
|
+
geospatial — drop the file into [geojson.io](https://geojson.io), QGIS,
|
|
260
|
+
kepler.gl, or Google Earth with no conversion. `--format csv|xlsx|json` also
|
|
261
|
+
available.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Indicators
|
|
266
|
+
|
|
267
|
+
Findings are named indicators, not a single score. A score collapses independent
|
|
268
|
+
facts into one number and hides which fact drove it.
|
|
269
|
+
|
|
270
|
+
| Indicator | Meaning |
|
|
271
|
+
|---|---|
|
|
272
|
+
| `no_web_presence` | No website published |
|
|
273
|
+
| `website_dead` | Website published but unreachable (needs `--check-websites`) |
|
|
274
|
+
| `no_contact_details` | No phone, website, or email at all |
|
|
275
|
+
| `no_address` / `no_coordinates` | Incomplete location data |
|
|
276
|
+
| `operator_named` | The source names who runs it |
|
|
277
|
+
| `chain_affiliated` | Carries a brand tag |
|
|
278
|
+
| `registration_ref` | Carries a licence/registration reference |
|
|
279
|
+
| `wikidata_linked` | Hard link to a structured record |
|
|
280
|
+
| `stale_survey` | Map data not verified recently |
|
|
281
|
+
| `not_operational` | Source reports it closed |
|
|
282
|
+
| `shared_phone` / `shared_domain` | Written by `geosint links --tag` |
|
|
283
|
+
|
|
284
|
+
`confidence` is a separate number: the percentage of core fields actually
|
|
285
|
+
populated. It measures **record completeness**, not how interesting the entity
|
|
286
|
+
is — those are different questions and should not share a scale.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## What the sources can and cannot tell you
|
|
291
|
+
|
|
292
|
+
- **OSM has no rating, review, or operational-status data.** Those come back as
|
|
293
|
+
unknown, and every check skips rather than inventing a value. A blank never
|
|
294
|
+
reads as a zero.
|
|
295
|
+
- **Ways carry tags but not coordinates** in offline `pbf` mode (full geometry
|
|
296
|
+
assembly needs a location index and far more memory). They are flagged
|
|
297
|
+
`no_coordinates` so the gap is visible in the data rather than silently absent.
|
|
298
|
+
Nodes — the common case for POIs — carry their coordinates.
|
|
299
|
+
- **`--check-websites` is not an audit.** One GET to each entity's own published
|
|
300
|
+
homepage, once, recording only whether it answered. Nothing is read out of the
|
|
301
|
+
page, and it is never a crawl. Sites whose host resolves into private or
|
|
302
|
+
loopback address space are declined rather than fetched — the URL comes from a
|
|
303
|
+
world-writable OSM tag.
|
|
304
|
+
- **Coverage varies enormously by region.** OSM is crowdsourced; absence of an
|
|
305
|
+
entity is weak evidence of absence in reality.
|
|
306
|
+
- **This tool does not scrape.** `pbf` reaches country scale through OSM's own
|
|
307
|
+
bulk distribution channel — a bigger free, ToS-clean source, not evasion of
|
|
308
|
+
anyone's rate limits. It does not harvest contact details off web pages
|
|
309
|
+
either: `email` comes only from the source's own `email`/`contact:email` tag,
|
|
310
|
+
published as a contact detail by a mapper.
|
|
311
|
+
- **Spreadsheet exports are formula-escaped.** An OSM `name` tag is
|
|
312
|
+
world-writable, and Excel evaluates a cell beginning `=`, `+`, `-` or `@`.
|
|
313
|
+
Values that would execute are prefixed so they stay text. GeoJSON and JSON are
|
|
314
|
+
untouched — nothing that reads them executes them.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Options (`collect` / `batch` / `pbf`)
|
|
319
|
+
|
|
320
|
+
| Flag | Default | What it does |
|
|
321
|
+
|---|---|---|
|
|
322
|
+
| `--what` | — | Category, e.g. `"pharmacy"` |
|
|
323
|
+
| `--osm-tag` | — | Any OSM tag, e.g. `"office=ngo"` (overrides `--what`) |
|
|
324
|
+
| `--area` | — | Place name, geocoded to a bounding box |
|
|
325
|
+
| `--near` / `--radius` | — | Centre point (`lat,lon`) and radius in km. google caps the radius at 50km |
|
|
326
|
+
| `--case` | `default` | Scopes entities, changes, and correlation |
|
|
327
|
+
| `--contact` | `$GEOSINT_CONTACT` | Your email/URL, sent as User-Agent |
|
|
328
|
+
| `--provider` | `osm` | `osm` (free, richer metadata) or `google` |
|
|
329
|
+
| `--tile` | 1 | osm only — N x N grid split for fuller coverage. Raise `--max-results` with it |
|
|
330
|
+
| `--check-websites` | off | Record whether each published site answers. 8 at a time |
|
|
331
|
+
| `--no-store` | off | One-off collection; disables change detection |
|
|
332
|
+
| `--db-path` | `.geosint.db` | Database location |
|
|
333
|
+
| `--max-results` | 60 | osm ceiling 3000; google hard cap 60 |
|
|
334
|
+
| `--min-confidence` | 0 | Drop records less than N% complete |
|
|
335
|
+
| `--format` | geojson | `geojson`, `csv`, `xlsx`, `json` |
|
|
336
|
+
| `--output` | — | Write results to this file |
|
|
337
|
+
| `--cache-ttl` | 24 | Hours before a cached query refetches (0 = never reuse) |
|
|
338
|
+
| `--no-cache` | off | Skip the cache entirely |
|
|
339
|
+
| `--dry-run` | off | Collect and analyse, write nothing |
|
|
340
|
+
| `--verbose` / `--quiet` | normal | Debug logging / warnings only |
|
|
341
|
+
|
|
342
|
+
A stored collection always bypasses the cache on read: it exists to answer
|
|
343
|
+
"what changed?", and only the network can answer that. The cache key covers
|
|
344
|
+
every flag that changes the result set, `--max-results` included, so raising a
|
|
345
|
+
cap refetches rather than replaying the smaller answer.
|
|
346
|
+
|
|
347
|
+
`--max-results` is applied **after** tiling merges and dedupes, so `--tile 3`
|
|
348
|
+
with the default 60 still keeps 60 — of the whole area, ranked by record
|
|
349
|
+
completeness, not the first tiles queried. When results are dropped to fit the
|
|
350
|
+
cap the run says so and names the flag to raise. A radius collection keeps the
|
|
351
|
+
nearest N instead, because there distance is a meaningful order.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## Development
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
git clone https://github.com/jenilmistryhq/geosint.git
|
|
359
|
+
cd geosint
|
|
360
|
+
pip install -e ".[dev,xlsx,dotenv]"
|
|
361
|
+
pytest -q
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
388 tests, all against mocked HTTP and temporary SQLite, or a real synthetic
|
|
365
|
+
`.osm.pbf` fixture — no API key, no network, no cost. (One skips unless the
|
|
366
|
+
`bulk` extra is installed.) Each test runs in its own working directory with a
|
|
367
|
+
neutralised `$HOME`, so nothing leaks between tests or into the repo.
|
|
368
|
+
|
|
369
|
+
Two rules this suite learned the hard way:
|
|
370
|
+
|
|
371
|
+
- **Build fixtures from a real normalizer, not by hand.** `dedupe` once keyed on
|
|
372
|
+
field names no normalizer produced. Every test passed; every branch of every
|
|
373
|
+
chain silently merged into one record. `tests/test_dedupe.py` now builds its
|
|
374
|
+
input by calling the collectors' own `_normalize`.
|
|
375
|
+
- **Mock the transport, assert on the contract.** Tests that mock a client
|
|
376
|
+
wholesale prove only that the mock works — `--provider google` had no
|
|
377
|
+
end-to-end test and silently discarded `--near` for as long as the flag
|
|
378
|
+
existed.
|
|
379
|
+
|
|
380
|
+
`tests/test_regressions.py` holds one named test per bug that produced
|
|
381
|
+
confident, wrong output.
|
|
382
|
+
|
|
383
|
+
### Layout
|
|
384
|
+
|
|
385
|
+
```
|
|
386
|
+
geosint/
|
|
387
|
+
├── cli.py argument parsing + command orchestration
|
|
388
|
+
├── wizard.py `start` - guided setup
|
|
389
|
+
├── display.py tables, bar charts, entity cards
|
|
390
|
+
├── config.py env/.env/CLI resolution + validation (one path, all commands)
|
|
391
|
+
├── useragent.py required contact -> User-Agent
|
|
392
|
+
├── geo.py coordinates, radius maths, GeoJSON
|
|
393
|
+
├── entities.py Entity record, indicators, confidence
|
|
394
|
+
├── analysis.py correlation: shared phone/domain/operator/location
|
|
395
|
+
├── db.py SQLite: entities, observations, collections, aliases, actions
|
|
396
|
+
├── osm_client.py Overpass + Nominatim collector
|
|
397
|
+
├── api_client.py Google Places API (New) collector
|
|
398
|
+
├── bulk_extract.py offline .osm.pbf extraction
|
|
399
|
+
├── dedupe.py identity keys, in-memory and at the DB boundary
|
|
400
|
+
├── website_health.py does the published site resolve
|
|
401
|
+
├── batch.py CSV multi-collection runner
|
|
402
|
+
├── cache.py per-query cache with a TTL
|
|
403
|
+
└── exporters.py GeoJSON / CSV / XLSX / JSON
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Data model
|
|
407
|
+
|
|
408
|
+
```
|
|
409
|
+
collections one row per run: query, source, area, counts, timestamps
|
|
410
|
+
|
|
|
411
|
+
entities current best-known state, one row per entity
|
|
412
|
+
|
|
|
413
|
+
observations APPEND-ONLY. Every field change, with source and timestamp.
|
|
414
|
+
aliases maps a second source's id onto the canonical entity
|
|
415
|
+
actions analyst triage changes and notes
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Adding a category? Add it to `CATEGORY_TAG_MAP` in `osm_client.py` plus a line
|
|
419
|
+
in `test_osm_client.py`.
|
|
420
|
+
|
|
421
|
+
## Legal and ethical note
|
|
422
|
+
|
|
423
|
+
This queries each data source's own official public API (OpenStreetMap's
|
|
424
|
+
Nominatim and Overpass, Google's Places API) and, with `--check-websites`, makes
|
|
425
|
+
a plain GET to an entity's own published homepage. It does not scrape search
|
|
426
|
+
results, bypass authentication or CAPTCHAs, evade rate limits, or attempt to
|
|
427
|
+
disguise itself — the contact address is mandatory precisely so it does not.
|
|
428
|
+
|
|
429
|
+
You are responsible for the lawfulness and proportionality of your collection.
|
|
430
|
+
Public availability is not the same as a right to compile, and the answer
|
|
431
|
+
differs by jurisdiction and by purpose. Where collected records touch personal
|
|
432
|
+
data, data-protection law generally applies to you as the one compiling them.
|
|
433
|
+
|
|
434
|
+
## License
|
|
435
|
+
|
|
436
|
+
[MIT](LICENSE)
|