Skip to content

Latest commit

Β 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ‡ͺπŸ‡Έ Spain Reverse Geocoder

Build & Test Java 21 No Dependencies License

Offline, zero-dependency reverse geocoding and municipality search for Spain using exact municipality polygons and a quadtree spatial index.

Given coordinates β†’ returns the municipality, province, autonomous community, and the municipality polygon (GeoJSON geometry).


✨ Features

Feature Description
Reverse Geocoding Exact point-in-polygon lookup using a quadtree spatial index
Municipality Search Find municipalities by name (exact or partial), accent & case insensitive
Streaming Parser Iterative, non-recursive JSON parser (no StackOverflowError)
Virtual Threads High-performance Demo Server scaling via Java 21 Virtual Threads
Batch Processing Bulk reverse geocode coordinates from CSV/text files
Web Demo Integrated interactive map for visual geocoding and search
Embedded Data ~92 MB GeoJSON with all 8,131 Spanish municipalities bundled in the JAR
Library API Clean builder-pattern API using Java 21 Records
CLI Tool Advanced command-line interface for all operations
Geometry Output Returns full municipality polygons as GeoJSON
Low Precision Mode Option to use ~75MB GeoJSON to save space
Zero Dependencies Pure Java 21 β€” no external libraries or build tools required

πŸš€ Quick Start

As a Library

import com.futesat.spaingeo.SpainGeo;
import com.futesat.spaingeo.model.ReverseGeocodeResult;

// 1. Build (loads embedded GeoJSON automatically)
SpainGeo geo = SpainGeo.builder().build();

// 2. Reverse geocode
ReverseGeocodeResult result = geo.reverse(40.4167, -3.70325);
System.out.println(result.municipality().name()); // β†’ "Madrid"

// 3. Search by name
List<ReverseGeocodeResult> results = geo.searchByName("CΓ³rdoba");

As a CLI Tool

# Reverse geocode
java -jar spain-reverse-geocoder.jar lookup --lat 40.4167 --lon -3.70325

# Search by name
java -jar spain-reverse-geocoder.jar search --name "Madrid"

# Launch web demo (Scalable via Virtual Threads)
java -jar spain-reverse-geocoder.jar demo

🌐 Interactive Web Demo

The project includes a built-in, zero-dependency web server optimized for Java 21.

java -jar spain-reverse-geocoder.jar demo [--port 8080]

Features:

  • Click-to-Geocode: Click anywhere on the map to find the municipality.
  • Search Bar: Search municipalities by name with real-time suggestions.
  • Visual Highlight: Shows exact borders (polygon) of the selected municipality.
  • Scalable: Uses Java 21 Virtual Threads to handle many concurrent users with minimal overhead.
  • Offline: Port 8080 by default, no external API keys required.

πŸ“¦ Build

Requires JDK 21+. No build tools needed.

./scripts/build.sh

Output:

  • build/spain-reverse-geocoder.jar β€” self-contained JAR with embedded GeoJSON data

πŸ“– Library API Reference

Initialization

// Default: loads all municipalities from embedded GeoJSON
SpainGeo geo = SpainGeo.builder().build();

// Load only specific provinces (saves memory)
SpainGeo geo = SpainGeo.builder()
    .provinces("28", "08", "46")  // Madrid, Barcelona, Valencia
    .build();

// Use your own GeoJSON file
SpainGeo geo = SpainGeo.builder()
    .geoJsonPath(Path.of("/path/to/municipalities.geojson"))
    .build();

// Use low-precision mode (saves ~15MB JAR space and memory)
SpainGeo geo = SpainGeo.builder()
    .lowPrecision(true)
    .build();

Reverse Geocoding

ReverseGeocodeResult result = geo.reverse(40.4167, -3.70325);

if (result != null) {
    result.municipality().id();    // "28079"
    result.municipality().name();  // "Madrid"
    result.province().id();        // "28"
    result.province().name();      // "Madrid"
    result.autonomousCommunity().id();    // "13"
    result.autonomousCommunity().name();  // "Madrid, Comunidad de"
    result.geometry().toJson();    // GeoJSON Polygon/MultiPolygon
}

Municipality Search

All search methods are accent-insensitive and case-insensitive. Searching for "cordoba" will match "CΓ³rdoba", "CΓ“RDOBA", etc.

// Exact name match
List<ReverseGeocodeResult> results = geo.searchByName("CΓ³rdoba");

// Partial/substring match
List<ReverseGeocodeResult> results = geo.searchByNameContains("madri");

// Filter by province + municipality name
List<ReverseGeocodeResult> results = geo.search("Madrid", "Getafe");
List<ReverseGeocodeResult> results = geo.search("28", "Getafe");  // by province code

// Listing
List<AdminDivision> communities = geo.listCommunities();
List<AdminDivision> provinces = geo.listProvinces("13"); // Madrid
List<ReverseGeocodeResult> municipalities = geo.listMunicipalitiesByProvince("28");

Province Codes

Province filtering uses 2-digit INE codes. Some common ones:

Code Province Code Province
01 Álava 28 Madrid
08 Barcelona 29 MΓ‘laga
11 CΓ‘diz 33 Asturias
15 A CoruΓ±a 41 Sevilla
20 Gipuzkoa 46 Valencia
48 Bizkaia 50 Zaragoza
51 Ceuta 52 Melilla

πŸ–₯️ CLI Reference

list β€” Administrative Browsing

List autonomous communities, provinces, and municipalities.

# List all autonomous communities
java -jar spain-reverse-geocoder.jar list communities

# List provinces in a specific community (e.g. 13 - Madrid)
java -jar spain-reverse-geocoder.jar list provinces --community 13

# List all municipalities in a province (e.g. 28 - Madrid)
java -jar spain-reverse-geocoder.jar list municipalities --province 28

# Include full GeoJSON polygons in the listing
java -jar spain-reverse-geocoder.jar list municipalities --province 28 --geometry

lookup β€” Reverse Geocode

java -jar spain-reverse-geocoder.jar lookup \
  --lat 40.4167 \
  --lon -3.70325

Output:

{
  "municipality": { "id": "28079", "name": "Madrid" },
  "province": { "id": "28", "name": "Madrid" },
  "autonomousCommunity": { "id": "13", "name": "Madrid, Comunidad de" },
  "geometry": {"type":"Polygon","coordinates":[...]}
}

search β€” Find Municipalities by Name

# Exact match
java -jar spain-reverse-geocoder.jar search --name "Madrid"

# Partial match
java -jar spain-reverse-geocoder.jar search --name "madri" --partial

# Filter by province
java -jar spain-reverse-geocoder.jar search --province "Madrid" --name "Getafe"

batch β€” Bulk Processing

Efficiently process multiple coordinates from a file.

java -jar spain-reverse-geocoder.jar batch --in points.csv

demo β€” Web Interface

Launch the interactive map.

java -jar spain-reverse-geocoder.jar demo [--port 8080]

Common Options

Option Description
--geojson <path> Path to custom GeoJSON file (default: embedded)
--provinces <codes> Comma-separated province codes to load (e.g. 28,08)
--mapping <path> Path to property mapping JSON
--catalog <path> Path to administrative catalog JSON
--low-precision Use 4-decimal precision data (~75MB vs ~90MB)

πŸ—οΈ Architecture

src/main/java/com/futesat/spaingeo/
β”œβ”€β”€ SpainGeo.java                    # Public library API (builder pattern)
β”œβ”€β”€ cli/
β”‚   └── Main.java                    # CLI entry point (lookup + search + demo)
β”œβ”€β”€ demo/
β”‚   └── DemoServer.java              # Virtual thread-based HTTP server
β”œβ”€β”€ core/
β”‚   β”œβ”€β”€ SpainReverseGeocoder.java    # Reverse geocoding engine
β”‚   β”œβ”€β”€ QuadtreeSpatialIndex.java    # Quadtree spatial index
β”‚   β”œβ”€β”€ MunicipalityFeature.java     # Feature record (result + geometry)
β”‚   β”œβ”€β”€ MunicipalityIndex.java       # Name search index
β”‚   └── TextNormalizer.java          # Accent/case-insensitive normalization
β”œβ”€β”€ geo/
β”‚   β”œβ”€β”€ Geometry.java                # Geometry interface
β”‚   β”œβ”€β”€ PolygonGeometry.java         # Polygon implementation
β”‚   β”œβ”€β”€ MultiPolygonGeometry.java    # MultiPolygon implementation
β”‚   β”œβ”€β”€ Ring.java                    # Ring (coordinate sequence)
β”‚   β”œβ”€β”€ Coordinate.java              # (x, y) coordinate record
β”‚   └── Envelope.java                # Bounding box
β”œβ”€β”€ io/
β”‚   β”œβ”€β”€ GeoJsonLoader.java           # GeoJSON parser with province filtering
β”‚   β”œβ”€β”€ MiniJsonParser.java          # Zero-dependency JSON parser
β”‚   β”œβ”€β”€ PropertyMapping.java         # GeoJSON property name mapping
β”‚   β”œβ”€β”€ PropertyMappingLoader.java   # Mapping file loader
β”‚   └── SpainCatalog.java            # Province/community catalog
└── model/
    β”œβ”€β”€ ReverseGeocodeResult.java    # Result record
    β”œβ”€β”€ AdminDivision.java           # (id, name) record
    └── JsonEscaper.java             # JSON string escaper

How It Works

  1. Loading: GeoJSON FeatureCollection is parsed, optionally filtered by province code
  2. Indexing: Municipality polygons are inserted into a quadtree spatial index; names are indexed for search
  3. Reverse Geocoding: The quadtree narrows candidates by bounding box, then exact point-in-polygon tests find the match
  4. Name Search: Text is normalized (NFD decomposition removes accents, lowercased) for accent/case-insensitive matching

πŸ“Š Data Source

The embedded GeoJSON contains official municipal boundaries from:

Centro de Descargas CNIG β€” LΓ­mites municipales, provinciales y autonΓ³micos (IGN/CNIG)

To use your own data, convert to GeoJSON with ogr2ogr:

ogr2ogr -f GeoJSON municipalities.geojson MUNICIPIOS.shp

Property Detection

The loader auto-detects common property names:

Field Detected Names
Municipality ID municipalityId, municipality_id, id, CODIGO, CMUNI, CUMUN, INE_MUNI, CODMUN
Municipality Name municipalityName, municipality_name, name, LITERAL, MUNICIPIO, NOMBRE, NMUNI
Province ID provinceId, province_id, CPRO, CPROV, INE_PROV, CODPROV
Province Name provinceName, province_name, PROVINCIA, NPRO, NPROV

Override with a custom mapping JSON file via --mapping.


πŸ§ͺ Testing

./scripts/test.sh

The test suite covers:

  • Reverse geocoding (positive + negative)
  • Polygon with holes
  • Province filtering
  • Text normalization (accents, Γ±, ΓΌ, Γ§)
  • Name search (exact, partial, case-insensitive, accent-insensitive)
  • Province + name search
  • Geometry JSON serialization
  • Catalog lookups
  • JSON escaping
  • Builder API

πŸ”§ CI/CD

GitHub Actions automatically builds and tests on every push to main and on pull requests. The JAR is uploaded as a build artifact.


πŸ“ Notes

  • Coordinates use WGS84 (longitude/latitude in decimal degrees)
  • Points on polygon borders are treated as inside
  • The algorithm handles both Polygon and MultiPolygon geometries
  • Province/community data is inferred from the municipality code (first 2 digits = province INE code) when not present in properties

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE.md file for details.

About

Offline, zero-dependency reverse geocoding and municipality search for Spain using exact municipality polygons and a quadtree spatial index.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages