Skip to content

Commit bb9ae79

Browse files
committed
Add Arabic language support
1 parent bc971fd commit bb9ae79

12 files changed

Lines changed: 961 additions & 265 deletions

.gitignore

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1-
/.vscode/
1+
*.zip
2+
*.gz
3+
__pycache__/
4+
*.pyc
5+
check_zips.py
6+
create_mock_data.py
7+
test_import.py
8+
.venv/

README.md

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
1-
# Reverse Geocode
1+
# Reverse Geocode (Arabic Version)
2+
3+
[![Arabic](https://img.shields.io/badge/Language-Arabic-green)](README_AR.md)
4+
[**اقرأ هذا الملف باللغة العربية**](README_AR.md)
5+
6+
Reverse Geocode takes a latitude / longitude coordinate and returns the nearest known country, state, and city in Arabic (where available).
7+
This library is a fork of the original `reverse_geocode` library, customized to provide output in Arabic.
8+
9+
## Features
10+
- **Offline Support**: Works completely offline with a local database.
11+
- **Arabic Output**: Returns country and city names in Arabic.
12+
- **Fast Lookup**: Uses a k-d tree for efficient nearest neighbour search.
213

3-
Reverse Geocode takes a latitude / longitude coordinate and returns the nearest known country, state, and city.
414
This can be useful when you need to reverse geocode a large number of coordinates so a web API is not practical.
515

616
The geocoded locations are from [geonames](http://download.geonames.org/export/dump/). This data is then structured in to a [k-d tree](http://en.wikipedia.org/wiki/K-d_tree>) for efficiently finding the nearest neighbour.
@@ -16,15 +26,15 @@ Example reverse geocoding a coordinate:
1626
>>> import reverse_geocode
1727
>>> melbourne_coord = -37.81, 144.96
1828
>>> reverse_geocode.get(melbourne_coord)
19-
{'country_code': 'AU', 'city': 'Melbourne', 'latitude': -37.814, 'longitude': 144.96332, 'population': 4917750, 'state': 'Victoria', 'country': 'Australia'}
29+
{'country_code': 'AU', 'city': 'ملبورن', 'latitude': -37.814, 'longitude': 144.96332, 'population': 4917750, 'state': 'Victoria', 'country': 'أستراليا'}
2030
```
2131

2232
Example reverse geocoding a list of coordinates:
2333
```
2434
>>> nyc_coord = 40.71427000, -74.00597000
2535
>>> reverse_geocode.search((melbourne_coord, nyc_coord))
26-
[{'country_code': 'AU', 'city': 'Melbourne', 'latitude': -37.814, 'longitude': 144.96332, 'population': 4917750, 'state': 'Victoria', 'country': 'Australia'},
27-
{'country_code': 'US', 'city': 'New York City', 'latitude': 40.71427, 'longitude': -74.00597, 'population': 8804190, 'state': 'New York', 'country': 'United States'}]
36+
[{'country_code': 'AU', 'city': 'ملبورن', 'latitude': -37.814, 'longitude': 144.96332, 'population': 4917750, 'state': 'Victoria', 'country': 'أستراليا'},
37+
{'country_code': 'US', 'city': 'نيويورك', 'latitude': 40.71427, 'longitude': -74.00597, 'population': 8804190, 'state': 'New York', 'country': 'الولايات المتحدة'}]
2838
```
2939

3040
By default the nearest known location is returned, which may not be as expected when there is a much larger city nearby.
@@ -44,6 +54,30 @@ To filter for larger cities a minimum population can be set. Using a minimum pop
4454
```
4555

4656

57+
```
58+
59+
## Offline Usage and Local Database
60+
61+
This library is designed to work completely offline after the initial setup.
62+
63+
### How it works:
64+
1. **Raw Data**: The library relies on data files from [GeoNames](http://download.geonames.org/export/dump/):
65+
- `cities1000.zip`: Contains city information.
66+
- `alternateNames.zip`: Contains alternate names (including Arabic).
67+
68+
2. **Local Database**: When run for the first time, the library extracts these files and creates a compact, fast-loading local file named `geocode.gz` in the library directory. This file serves as the "local database".
69+
70+
### First Run:
71+
- **With Internet**: The library will automatically download the required files (`cities1000.zip` and `alternateNames.zip`) and generate `geocode.gz`.
72+
- **Without Internet (Manual)**:
73+
1. Download the following two files from an internet-connected machine:
74+
- [http://download.geonames.org/export/dump/cities1000.zip](http://download.geonames.org/export/dump/cities1000.zip)
75+
- [http://download.geonames.org/export/dump/alternateNames.zip](http://download.geonames.org/export/dump/alternateNames.zip)
76+
2. Place these two files in the project's root directory (or current working directory).
77+
3. When you run the code for the first time, the library will detect these files and generate `geocode.gz` from them without needing an internet connection.
78+
79+
Once `geocode.gz` is created, the zip files and internet connection are no longer needed. You can move `geocode.gz` along with your code to any other machine and it will work immediately.
80+
4781
## Install
4882
4983
```

README.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
Reverse Geocode (Arabic Version)
2+
===============
3+
4+
Reverse Geocode takes a latitude / longitude coordinate and returns the country and city in Arabic (where available).
5+
Example usage:
6+
7+
.. sourcecode:: python
8+
9+
>>> import reverse_geocode
10+
>>> coordinates = (-37.81, 144.96), (31.76, 35.21)
11+
>>> reverse_geocode.search(coordinates)
12+
[{'city': 'ملبورن', 'country_code': 'AU', 'country': 'أستراليا'},
13+
{'city': 'القدس', 'country_code': 'IL', 'country': 'إسرائيل'}]
14+
15+
..
16+
17+
The module has a set of known geocoded locations and uses a `k-d tree <http://en.wikipedia.org/wiki/K-d_tree>`_ to efficiently find the nearest neighbour. This can be useful when you need to reverse geocode a large number of coordinates so a web API is not practical.
18+
19+
As this is a point based and not a polygon based lookup it will only give a rough idea of the location/city
20+
21+
=======
22+
Install
23+
=======
24+
25+
Supports python 2 & 3:
26+
27+
.. sourcecode:: bash
28+
29+
pip install reverse-geocode
30+
pip3 install reverse-geocode
31+
32+
..

README_AR.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# البحث العكسي عن الموقع الجغرافي (نسخة عربية)
2+
3+
[![English](https://img.shields.io/badge/Language-English-blue)](README.md)
4+
[**Read this document in English**](README.md)
5+
6+
تقوم المكتبة بأخذ إحداثيات (خط العرض / خط الطول) وتُرجع أقرب بلد، مدينة، وولاية معروفة باللغة العربية (حيثما توفرت).
7+
هذه المكتبة هي نسخة معدلة من مكتبة `reverse_geocode` الأصلية، تم تخصيصها لتعطي النتائج باللغة العربية.
8+
9+
## المميزات
10+
- **دعم العمل بدون إنترنت**: تعمل بالكامل بدون اتصال بعد التحضير الأولي.
11+
- **مخرجات عربية**: تُرجع أسماء الدول والمدن باللغة العربية.
12+
- **بحث سريع**: تستخدم خوارزمية k-d tree للبحث السريع عن أقرب موقع.
13+
14+
يمكن أن يكون هذا مفيدًا عندما تحتاج إلى إجراء بحث عكسي لعدد كبير من الإحداثيات بحيث لا يكون استخدام واجهة برمجة تطبيقات الويب (Web API) عمليًا.
15+
16+
المواقع الجغرافية مأخوذة من [geonames](http://download.geonames.org/export/dump/). يتم تنظيم البيانات في [k-d tree](http://en.wikipedia.org/wiki/K-d_tree>) للبحث الفعال عن أقرب جار.
17+
18+
لاحظ أنه نظرًا لأن هذا البحث يعتمد على النقاط وليس المضلعات، فإنه سيعطي فكرة تقريبية فقط عن الموقع/المدينة.
19+
20+
## أمثلة الاستخدام
21+
22+
مثال على البحث العكسي لإحداثي:
23+
24+
```
25+
>>> import reverse_geocode
26+
>>> melbourne_coord = -37.81, 144.96
27+
>>> reverse_geocode.get(melbourne_coord)
28+
{'country_code': 'AU', 'city': 'ملبورن', 'latitude': -37.814, 'longitude': 144.96332, 'population': 4917750, 'state': 'Victoria', 'country': 'أستراليا'}
29+
```
30+
31+
مثال على البحث العكسي لقائمة من الإحداثيات:
32+
```
33+
>>> nyc_coord = 40.71427000, -74.00597000
34+
>>> reverse_geocode.search((melbourne_coord, nyc_coord))
35+
[{'country_code': 'AU', 'city': 'ملبورن', 'latitude': -37.814, 'longitude': 144.96332, 'population': 4917750, 'state': 'Victoria', 'country': 'أستراليا'},
36+
{'country_code': 'US', 'city': 'نيويورك', 'latitude': 40.71427, 'longitude': -74.00597, 'population': 8804190, 'state': 'New York', 'country': 'الولايات المتحدة'}]
37+
```
38+
39+
بشكل افتراضي، يتم إرجاع أقرب موقع معروف، وقد لا يكون ذلك كما هو متوقع عندما تكون هناك مدينة أكبر بكثير في مكان قريب.
40+
للفرز حسب المدن الأكبر، يمكن تعيين حد أدنى للسكان.
41+
42+
```
43+
44+
## العمل بدون إنترنت (Offline) وقاعدة البيانات المحلية
45+
46+
هذه المكتبة مصممة للعمل بشكل كامل بدون إنترنت بعد التحضير الأولي.
47+
48+
### كيفية العمل:
49+
1. **البيانات الخام**: تعتمد المكتبة على ملفات بيانات من [GeoNames](http://download.geonames.org/export/dump/)، وهي:
50+
- `cities1000.zip`: يحتوي على معلومات المدن.
51+
- `alternateNames.zip`: يحتوي على الأسماء البديلة (بما في ذلك العربية).
52+
53+
2. **قاعدة البيانات المحلية**: عند تشغيل المكتبة لأول مرة، تقوم بفك ضغط هذه الملفات وإنشاء ملف محلي مضغوط وسريع التحميل يسمى `geocode.gz` داخل مجلد المكتبة. هذا الملف هو "قاعدة البيانات المحلية".
54+
55+
### التشغيل لأول مرة:
56+
- **مع وجود إنترنت**: ستقوم المكتبة تلقائيًا بتحميل الملفات المطلوبة (`cities1000.zip` و `alternateNames.zip`) وإنشاء `geocode.gz`.
57+
- **بدون إنترنت (يدويًا)**:
58+
1. قم بتحميل الملفين التاليين من جهاز متصل بالإنترنت:
59+
- [http://download.geonames.org/export/dump/cities1000.zip](http://download.geonames.org/export/dump/cities1000.zip)
60+
- [http://download.geonames.org/export/dump/alternateNames.zip](http://download.geonames.org/export/dump/alternateNames.zip)
61+
2. ضع الملفين في المجلد الرئيسي للمشروع (أو مسار العمل الحالي).
62+
3. عند تشغيل الكود لأول مرة، ستجد المكتبة هذه الملفات وستقوم بإنشاء `geocode.gz` منها دون الحاجة للاتصال بالإنترنت.
63+
64+
بمجرد إنشاء ملف `geocode.gz`، لا تعد هناك حاجة لملفات الـ zip أو للإنترنت. يمكنك نقل ملف `geocode.gz` مع الكود الخاص بك إلى أي جهاز آخر وسيعمل مباشرة.
65+
66+
## التثبيت
67+
68+
```
69+
pip install reverse-geocode
70+
```

__init__.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import csv, logging, os, sys, zipfile
4+
if sys.platform == 'win32':
5+
csv.field_size_limit(2**31-1)
6+
else:
7+
csv.field_size_limit(sys.maxsize)
8+
try:
9+
from urllib import urlretrieve
10+
except ImportError:
11+
from urllib.request import urlretrieve
12+
from scipy.spatial import cKDTree as KDTree
13+
14+
# location of geocode data to download
15+
GEOCODE_URL = 'http://download.geonames.org/export/dump/cities1000.zip'
16+
GEOCODE_FILENAME = 'cities1000.txt'
17+
ALTERNATE_NAMES_URL = 'http://download.geonames.org/export/dump/alternateNames.zip'
18+
ALTERNATE_NAMES_FILENAME = 'alternateNames.txt'
19+
20+
21+
def singleton(cls):
22+
"""Singleton pattern to avoid loading class multiple times
23+
"""
24+
instances = {}
25+
26+
def getinstance():
27+
if cls not in instances:
28+
instances[cls] = cls()
29+
return instances[cls]
30+
return getinstance
31+
32+
33+
@singleton
34+
class GeocodeData:
35+
36+
def __init__(self, geocode_filename='geocode.csv', country_filename='countries.csv'):
37+
coordinates, self.__locations = self.__extract(rel_path(geocode_filename))
38+
self.__tree = KDTree(coordinates)
39+
self.__load_countries(rel_path(country_filename))
40+
41+
def __load_countries(self, country_filename):
42+
"""Load a map of country code to name
43+
"""
44+
self.__countries = {}
45+
with open(country_filename, 'r') as handler:
46+
for code, name in csv.reader(handler):
47+
self.__countries[code] = name
48+
49+
def query(self, coordinates):
50+
"""Find closest match to this list of coordinates
51+
"""
52+
try:
53+
distances, indices = self.__tree.query(coordinates, k=1)
54+
except ValueError as e:
55+
logging.info('Unable to parse coordinates: {}'.format(coordinates))
56+
raise e
57+
else:
58+
results = [self.__locations[index] for index in indices]
59+
for result in results:
60+
result['country'] = self.__countries.get(result['country_code'], '')
61+
return results
62+
63+
def __download(self):
64+
"""Download geocode file
65+
"""
66+
for url in (GEOCODE_URL, ALTERNATE_NAMES_URL):
67+
local_filename = os.path.abspath(os.path.basename(url))
68+
if not os.path.exists(local_filename):
69+
logging.info('Downloading: {}'.format(url))
70+
urlretrieve(url, local_filename)
71+
return local_filename
72+
73+
def __extract(self, local_filename):
74+
"""Extract geocode data from zip
75+
"""
76+
if os.path.exists(local_filename):
77+
# open compact CSV
78+
rows = csv.reader(open(local_filename, 'r', encoding='utf-8'))
79+
else:
80+
if not os.path.exists(GEOCODE_FILENAME) or not os.path.exists(ALTERNATE_NAMES_FILENAME):
81+
# remove files to get updated data
82+
if os.path.exists(GEOCODE_FILENAME):
83+
os.remove(GEOCODE_FILENAME)
84+
if os.path.exists(ALTERNATE_NAMES_FILENAME):
85+
os.remove(ALTERNATE_NAMES_FILENAME)
86+
87+
for url, filename in ((GEOCODE_URL, GEOCODE_FILENAME), (ALTERNATE_NAMES_URL, ALTERNATE_NAMES_FILENAME)):
88+
downloadedFile = os.path.abspath(os.path.basename(url)) # Re-derive local filename
89+
if not os.path.exists(downloadedFile): # Check if already downloaded in __download loop? No, this is clearer.
90+
downloadedFile = self.__download() # Wait, __download downloads BOTH now.
91+
92+
# Actually, let's simplify. __download ensures files are there.
93+
# Just unzip them.
94+
pass
95+
96+
# Re-do this logic properly.
97+
# 1. Download both if missing on extraction needed.
98+
self.__download()
99+
100+
logging.info('Extracting: {}'.format(GEOCODE_FILENAME))
101+
with zipfile.ZipFile(os.path.abspath(os.path.basename(GEOCODE_URL))) as z:
102+
with open(GEOCODE_FILENAME, 'wb') as fp:
103+
fp.write(z.read(GEOCODE_FILENAME))
104+
105+
logging.info('Extracting: {}'.format(ALTERNATE_NAMES_FILENAME))
106+
with zipfile.ZipFile(os.path.abspath(os.path.basename(ALTERNATE_NAMES_URL))) as z:
107+
with open(ALTERNATE_NAMES_FILENAME, 'wb') as fp:
108+
fp.write(z.read(ALTERNATE_NAMES_FILENAME))
109+
110+
# Load Arabic names
111+
arabic_names = {}
112+
logging.info('Loading Arabic names...')
113+
with open(ALTERNATE_NAMES_FILENAME, 'r', encoding='utf-8') as f:
114+
reader = csv.reader(f, delimiter='\t')
115+
for row in reader:
116+
# alternateNameId, geonameId, isolanguage, alternateName, isPreferred, isShort, isColloquial, isHistoric
117+
if len(row) > 3 and row[2] == 'ar':
118+
geoname_id = row[1]
119+
arabic_name = row[3]
120+
# Prefer short names or just overwrite? Let's just take the first one or overwrite.
121+
# Maybe prefer if isPreferred? row[4]
122+
arabic_names[geoname_id] = arabic_name
123+
124+
# extract coordinates into more compact CSV for faster loading
125+
writer = csv.writer(open(local_filename, 'w', encoding='utf-8', newline=''))
126+
rows = []
127+
for row in csv.reader(open(GEOCODE_FILENAME, 'r', encoding='utf-8'), delimiter='\t'):
128+
geoname_id = row[0]
129+
latitude, longitude = row[4:6]
130+
country_code = row[8]
131+
if latitude and longitude and country_code:
132+
city = row[1]
133+
if geoname_id in arabic_names:
134+
city = arabic_names[geoname_id]
135+
136+
row = latitude, longitude, country_code, city
137+
writer.writerow(row)
138+
rows.append(row)
139+
140+
# cleanup downloaded files? Maybe keep them for caching if user wants to rebuild.
141+
#Original code cleaned up. Let's keep it clean.
142+
for filename in (os.path.basename(GEOCODE_URL), os.path.basename(ALTERNATE_NAMES_URL), GEOCODE_FILENAME, ALTERNATE_NAMES_FILENAME):
143+
if os.path.exists(filename):
144+
os.remove(filename)
145+
146+
# load a list of known coordinates and corresponding __locations
147+
coordinates, __locations = [], []
148+
for latitude, longitude, country_code, city in rows:
149+
coordinates.append((latitude, longitude))
150+
__locations.append(dict(country_code=country_code, city=city))
151+
return coordinates, __locations
152+
153+
154+
def rel_path(filename):
155+
"""Return the path of this filename relative to the current script
156+
"""
157+
return os.path.join(os.getcwd(), os.path.dirname(__file__), filename)
158+
159+
160+
def get(coordinate):
161+
"""Search for closest known location to this coordinate
162+
"""
163+
gd = GeocodeData()
164+
return gd.query([coordinate])[0]
165+
166+
167+
def search(coordinates):
168+
"""Search for closest known locations to these coordinates
169+
"""
170+
gd = GeocodeData()
171+
return gd.query(coordinates)
172+
173+
174+
if __name__ == '__main__':
175+
# test some coordinate lookups
176+
city1 = -37.81, 144.96
177+
city2 = 31.76, 35.21
178+
print(get(city1))
179+
print(search([city1, city2]))

0 commit comments

Comments
 (0)