geocsv
Turn any spreadsheet or CSV of places into a map-ready GeoJSON, GeoPackage, KML or GPX file, without GDAL.
You have a spreadsheet export with a column called Breitengrad, or enlem;boylam
with decimal commas, or 40°26′46″N 79°58′56″W strings, or Plus Codes, or MGRS
references. geocsv finds the coordinates, tells you how sure it is and why, reports the
rows that are broken, and writes a file that QGIS, ArcGIS, Google Earth, a GPS or a web
map can open. It is pure Python with no required dependencies.
📦 Installation
pip install geocsv
# optional extras
pip install "geocsv[pyproj]" # reproject x/y or EWKT from any EPSG code
pip install "geocsv[shapefile]" # write ESRI Shapefiles (via pyshp)
Python 3.9+ on Windows, macOS or Linux.
⚡ Quickstart
geocsv inspect places.csv # what was detected, and which rows are wrong
geocsv convert places.csv -o places.gpkg # GeoPackage (also .geojson .geojsonl .kml .gpx .shp)
geocsv convert places.csv -o places.geojson --lat "Y koord" --lon "X koord"
import geocsv
table = geocsv.read("places.csv")
print(table.detection.describe())
# latlon: lat='Breitengrad', lon='Längengrad', decimal (confidence 0.99)
print(table.report())
table.to_gpkg("places.gpkg")
table.to_geojson("places.geojson")
fc = table.to_geojson() # or get the FeatureCollection as a dict
table.to_kml("places.kml")
table.to_gpx("places.gpx")
geocsv.detect("places.csv") # detection only, with .reasons and .alternatives
🔎 What it reads
CSV dialects (standard library only)
- encodings: UTF-8, UTF-8 with BOM, UTF-16 (Excel "Unicode text"), and the Windows code pages cp1252 / latin-1 / cp1254 (Turkish letters such as ğ ı ş are told apart from Western European text)
- delimiters
,;tab|(sniffed from the data, quoted fields respected) and Excel'ssep=;first line - decimal commas (
40,7128), header or no header (detected), ragged rows
Geometry encodings, found automatically:
| kind | example | notes |
|---|---|---|
latlon |
lat / lon columns |
header names in many languages: lat, latitude, latitud, Breitengrad, enlem, широта, lng, long, Längengrad, boylam, долгота, … plus value ranges when names are missing |
latlon (DMS) |
40°26′46″N, N40 26.767, 40d26m46s |
degrees, minutes, seconds, any symbols or letters |
pair |
"41.0082, 28.9784", 40°26′46″N 79°58′56″W |
one column; lat/lon order inferred from ranges, hemisphere letters or a lonlat header |
wkt |
POINT (29 41), SRID=3857;POLYGON ((…)) |
all simple-feature types, Z/M/ZM, EWKT |
geojson |
{"type": "Point", "coordinates": [29, 41]} |
geometry or Feature objects |
geohash |
sxk97cn4 |
|
pluscode |
8GFJWVM5+9V |
Open Location Code; short codes with plus_code_reference |
maidenhead |
KM69kw |
2-10 characters |
mgrs |
35T PF 66370 41552 |
WGS 84, pure-Python Krüger series |
utm |
35T 666371 4541552 |
zone + band or hemisphere |
xy |
easting / northing columns |
any EPSG code via the pyproj extra |
Every detection has a confidence and human-readable reasons:
$ geocsv detect stations.csv
latlon: lat='lon', lon='lat', decimal (confidence 0.99)
- header 'lat' looks like latitude but its values only fit longitude; the columns are swapped
- 100% of values are coordinates; 100% are within latitude/longitude range
- the two columns are adjacent
Override any decision: lat=/lon=, wkt=, column= + kind=, x=/y= + epsg=,
order="lonlat", encoding=, delimiter=, header= (CLI: --lat, --lon, --wkt,
--column, --kind, --x, --y, --epsg, --order, --encoding, --delimiter,
--header/--no-header). Columns can be named or numbered from 1.
🩺 Validation report
$ geocsv inspect issues.csv
Geometry latlon: lat='latitude', lon='longitude', decimal (confidence 0.87)
Valid 8 of 13 rows bbox=(0.000000, 0.000000, 37.874600, 41.008200)
issue severity rows lines
-------------- -------- ----- --------------------
missing error 2 3, 4
invalid error 1 5
out_of_range error 1 6
swapped error 1 9
likely_swapped warning 1 13
null_island warning 1 7
duplicate warning 1 8
ragged warning 1 14
| code | severity | meaning |
|---|---|---|
missing |
error | empty geometry cell(s) |
invalid |
error | the value does not parse |
empty |
error | WKT/GeoJSON EMPTY geometry |
out_of_range |
error | outside ±90 / ±180 |
swapped |
error | latitude beyond ±90 but would fit if swapped (fix_swapped=True repairs it) |
crs |
error | EWKT in another CRS and pyproj is not installed |
likely_swapped |
warning | in range, but far closer to the rest of the table when swapped |
swapped_fixed |
warning | swapped back (by fix_swapped=True or hemisphere letters) |
null_island |
warning | exactly (0, 0) |
duplicate |
warning | same geometry as an earlier row |
ragged |
warning | row length differs from the header |
Line numbers are physical lines in the file, as your editor shows them.
table.issues, table.summary() and geocsv inspect --json give the same data in
machine-readable form; geocsv convert --strict exits with status 1 when any row has an
error.
🗺️ What it writes
| format | method | notes |
|---|---|---|
| GeoJSON | to_geojson(path=None) |
RFC 7946 FeatureCollection; returns a dict without a path |
| GeoJSONSeq | to_geojsonseq(path) |
one Feature per line (.geojsonl, .ndjson) |
| GeoPackage | to_gpkg(path, layer=None) |
pure sqlite3; OGC GeoPackage 1.4 tables, StandardGeoPackageBinary + ISO WKB, any geometry type, Z |
| KML | to_kml(path=None) |
Placemarks with ExtendedData, holes, MultiGeometry |
| GPX | to_gpx(path=None) |
points as waypoints, lines as tracks |
| Shapefile | to_shapefile(path) |
needs geocsv[shapefile]; one geometry family |
| any | to_file(path, driver=None) |
picks the format from the extension |
Attributes keep their types: integer, real or text (codes like "00123" stay text).
A name-like column (name, title, isim, nombre, …) labels KML placemarks and GPX
waypoints. The GeoPackage writer is tested against sqlite3, GDAL's ogrinfo, GDAL's
validate_gpkg.py conformance checker and GeoPandas.
🧭 Coordinate helpers
The decoders are public and dependency-free:
from geocsv import dms, geohash, pluscode, maidenhead, utm
dms.parse_dms_pair("40°26′46″N 79°58′56″W") # (40.4461, -79.9822)
dms.format_dms(40.446111, "lat") # '40°26′46.0″N'
geohash.decode("u4pruydqqvj") # (57.64911, 10.40744)
pluscode.decode("849VCWC8+R9") # (37.4220625, -122.0840625)
pluscode.recover_nearest("CWC8+R9", 37.4, -122.0)
maidenhead.decode("FN31pr") # (41.7292, -72.7083)
utm.mgrs_to_latlon("31U DQ 48251 11932") # (48.8582, 2.2945)
utm.latlon_to_mgrs(48.8582, 2.2945) # '31U DQ 48251 11932'
utm.utm_to_latlon(31, "N", 448251, 5411932)
UTM/MGRS use Krüger's series to sixth order; they agree with PROJ to under a millimetre.
🛠️ Development
git clone https://github.com/YusufEminoglu/geocsv_sdk.git
cd geocsv_sdk
pip install -e ".[dev,pyproj,shapefile]"
python -m pytest --cov=geocsv
python -m ruff check . && python -m ruff format --check src tests && python -m mypy src
The tests include hand-made fixture CSVs, reference values for every decoder, a seeded synthetic corpus of 400 tables that measures detection accuracy, and GDAL-based checks of the GeoPackage output that run when GDAL is installed.
📄 License
MIT © Yusuf Eminoğlu
Metadata
Release files for geocsv 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| geocsv-0.1.1.tar.gz | 81.5 kB | With provenance | Details for geocsv-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| geocsv-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for geocsv-0.1.1-py3-none-any.whl |
Total release size: 141.5 kB
Release files / geocsv-0.1.1.tar.gz
| Download URL | geocsv-0.1.1.tar.gz |
|---|---|
| Size | 81.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2d81d7d01d95de81946c48358052a095a3407c7a4e0b2cad2a5f7f72af69f69b
|
|
BLAKE2b-256 checksum How to use checksums |
e4fae8c6abe023c9c9b22b4c52519e8721905e41b5306b20d336adcc2ba27ed2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logRelease files / geocsv-0.1.1-py3-none-any.whl
| Download URL | geocsv-0.1.1-py3-none-any.whl |
|---|---|
| Size | 60.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6e739ebfeb788a894958271a154ac9a60f3575b5f98b923e249d98acdf9a1e83
|
|
BLAKE2b-256 checksum How to use checksums |
f4ae7eb66d8cd4d67ed9e237ff66c0412f89e066988e3208f12377fb44a17edf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logSecurity for geocsv 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for geocsv-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.