Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,85 @@ Create the PostGIS extension if needed

sudo -u postgres psql -c 'CREATE EXTENSION postgis;'

## Import or re-import data

The project includes convenient shell scripts for importing geospatial
data from various sources.

### Available import scripts

* `scripts/import-municipalities-data.sh` - Import municipalities from NLS (requires manual download)
* `scripts/import-digiroad-data.sh` - Import address data from Digiroad / Finnish Transport Infrastructure Agency
* `scripts/import-paavo-data.sh` - Import postal code areas from Paavo / Statistics Finland
* `scripts/import-post-office-data.sh` - Import post office names from Posti
* `scripts/delete-address-data.sh` - Delete all address data (with confirmation)

### First-time import

**Important:** Municipality data must be imported first, before importing addresses.

#### 1. Import municipalities (required, manual download)

Municipality data must be manually downloaded from NLS:

1. Visit [NLS Administrative Areas](https://www.maanmittauslaitos.fi/en/maps-and-spatial-data/datasets-and-interfaces/product-descriptions/division-administrative-areas-vector)
2. Download the dataset following NLS's download process
3. Extract the ZIP file to a directory (e.g., `/tmp/nls/`)
4. Run the import script:

./scripts/import-municipalities-data.sh /tmp/nls/SuomenKuntajako_2026_10k.shp

#### 2. Import addresses and other data

After municipalities are imported, import other data:

# Import addresses (required, specify province)
./scripts/import-digiroad-data.sh uusimaa

# Import postal code areas (optional, specify province)
./scripts/import-paavo-data.sh uusimaa

# Import post office names (optional, downloads latest data)
./scripts/import-post-office-data.sh

Available provinces: `uusimaa` and `varsinais-suomi`

### Re-importing data

To re-import data (e.g., after updates):

# Delete existing address data (prompts for confirmation)
./scripts/delete-address-data.sh

# Re-import municipalities if needed
./scripts/import-municipalities-data.sh /path/to/SuomenKuntajako_2026_10k.shp

# Re-import other data
./scripts/import-digiroad-data.sh uusimaa
./scripts/import-paavo-data.sh uusimaa
./scripts/import-post-office-data.sh

### Manual import using Django commands

You can also use the Django management commands directly:

# Import municipalities (required first, manual download needed)
python manage.py import_municipalities <path-to-shapefile>

# Import addresses
python manage.py import_addresses <path-to-shapefiles> <province>

# Import postal code areas
python manage.py import_postal_code_areas <province> <path-to-shapefiles>

# Import post office names
python manage.py import_post_offices <path-to-zip-file>
# or download directly:
python manage.py import_post_offices --url <url-to-posti-zip>

# Delete all address data
python manage.py delete_address_data

## Keeping Python requirements up to date

1. Add new packages to `requirements.in` or `requirements-dev.in`
Expand Down
28 changes: 25 additions & 3 deletions address/admin.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from parler.admin import TranslatableAdmin

from .models import Address, Municipality, PostalCodeArea, Street


@admin.register(Municipality)
class MunicipalityAdmin(TranslatableAdmin):
pass
list_display = ("id", "code")
ordering = ("id",)
search_fields = ("id",)


@admin.register(Street)
class StreetAdmin(TranslatableAdmin):
pass
list_display = ("name_column", "municipality")
search_fields = ("translations__name",)

@admin.display(description=_("Name"))
def name_column(self, object):
return object.name

def get_queryset(self, request):
language_code = self.get_queryset_language(request)
return (
super()
.get_queryset(request)
.translated(language_code)
.order_by("translations__name")
)


@admin.register(PostalCodeArea)
class PostalCodeAreaAdmin(TranslatableAdmin):
pass
list_display = ("postal_code", "name")
ordering = ("postal_code",)
search_fields = ("postal_code",)


@admin.register(Address)
class AddressAdmin(admin.ModelAdmin):
list_display = ("street", "number", "municipality", "postal_code_area")
raw_id_fields = ["street"]
ordering = ("street", "number", "municipality")
search_fields = ("street__translations__name",)
2 changes: 1 addition & 1 deletion address/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class Meta:
class PostalCodeAreaSerializer(TranslatedAreaModelSerializer):
class Meta:
model = PostalCodeArea
fields = ["postal_code", "translations", "area", "municipality"]
fields = ["postal_code", "translations", "area"]


class AddressSerializer(serializers.ModelSerializer):
Expand Down
90 changes: 59 additions & 31 deletions address/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
name="streetname",
location=OpenApiParameter.QUERY,
description=(
"Street name in Finnish or Swedish. "
"Street name in Finnish, Swedish or English. "
'E.g. "Mannerheimintie" or "Mannerheimvägen".'
),
required=False,
Expand Down Expand Up @@ -97,8 +97,9 @@
OpenApiParameter(
name="geom_format",
location=OpenApiParameter.QUERY,
description="Area geometry format. "
"Available values : geojson, ewkt. Default : geojson",
description=(
"Area geometry format. Available values : geojson, ewkt. Default : geojson"
),
required=False,
type=str,
),
Expand All @@ -115,8 +116,20 @@
OpenApiParameter(
name="postalcodearea",
location=OpenApiParameter.QUERY,
description='Postal code area name in Finnish or Swedish. "'
'E.g. "Lappohja" or "Lappvik"',
description=(
'Postal code area name in Finnish, Swedish or English. "'
'E.g. "Lappohja" or "Lappvik"'
),
required=False,
type=str,
),
OpenApiParameter(
name="postoffice",
location=OpenApiParameter.QUERY,
description=(
"Post office name in Finnish, Swedish or English. "
'E.g. "HELSINKI", "HELSINGFORS".'
),
required=False,
type=str,
),
Expand All @@ -127,7 +140,8 @@
name="municipality",
location=OpenApiParameter.QUERY,
description=(
'Municipality name in Finnish or Swedish. E.g. "Helsinki" or "Helsingfors".'
"Municipality name in Finnish, Swedish or English. "
'E.g. "Helsinki" or "Helsingfors".'
),
required=False,
type=str,
Expand Down Expand Up @@ -167,6 +181,7 @@ def get_queryset(self) -> QuerySet:
addresses = self._filter_by_municipality(addresses)
addresses = self._filter_by_municipality_code(addresses)
addresses = self._filter_by_postal_code(addresses)
addresses = self._filter_by_postal_code_area(addresses)
addresses = self._filter_by_post_office(addresses)
addresses = self._filter_by_bbox(addresses)
addresses = self._filter_by_location(addresses)
Expand Down Expand Up @@ -225,13 +240,27 @@ def _filter_by_postal_code(self, addresses: QuerySet) -> QuerySet:
return addresses
return addresses.filter(postal_code_area__postal_code__iexact=postal_code)

def _filter_by_post_office(self, addresses: QuerySet) -> QuerySet:
def _filter_by_postal_code_area(self, addresses: QuerySet) -> QuerySet:
postal_code_area = self.request.query_params.get("postalcodearea")
if postal_code_area is None:
return addresses
return addresses.filter(
postal_code_area__translations__name__iexact=postal_code_area
).distinct()
postal_code_area_ids = list(
PostalCodeArea.objects.filter(
translations__name__iexact=postal_code_area
).values_list("id", flat=True)
)
return addresses.filter(postal_code_area_id__in=postal_code_area_ids)

def _filter_by_post_office(self, addresses: QuerySet) -> QuerySet:
post_office = self.request.query_params.get("postoffice")
if post_office is None:
return addresses
postal_code_area_ids = list(
PostalCodeArea.objects.filter(
translations__post_office__iexact=post_office
).values_list("id", flat=True)
)
return addresses.filter(postal_code_area_id__in=postal_code_area_ids)

def _filter_by_bbox(self, addresses: QuerySet) -> QuerySet:
bbox = self.request.query_params.get("bbox")
Expand Down Expand Up @@ -276,9 +305,7 @@ def _filter_by_location(self, addresses: QuerySet) -> QuerySet:


@extend_schema_view(
list=extend_schema(
parameters=_area_parameters + _postal_code_parameters + _municipality_parameters
),
list=extend_schema(parameters=_area_parameters + _postal_code_parameters),
retrieve=extend_schema(parameters=_area_parameters),
)
class PostalCodeAreaViewSet(ReadOnlyModelViewSet):
Expand All @@ -287,37 +314,38 @@ class PostalCodeAreaViewSet(ReadOnlyModelViewSet):

def get_queryset(self) -> QuerySet:
areas = self.queryset
areas = self._filter_by_municipality(areas)
areas = self._filter_by_municipality_code(areas)
areas = self._filter_by_postal_code(areas)
areas = self._filter_by_postal_code_area(areas)
areas = self._filter_by_post_office(areas)
return areas

def _filter_by_municipality(self, areas: QuerySet) -> QuerySet:
municipality = self.request.query_params.get("municipality")
if municipality is None:
return areas
return areas.filter(
municipality__translations__name__iexact=municipality
).distinct()

def _filter_by_municipality_code(self, areas: QuerySet) -> QuerySet:
municipality_code = self.request.query_params.get("municipalitycode")
if municipality_code is None:
return areas
return areas.filter(municipality__code__iexact=municipality_code).distinct()

def _filter_by_postal_code(self, areas: QuerySet) -> QuerySet:
postal_code = self.request.query_params.get("postalcode")
if postal_code is None:
return areas
return areas.filter(postal_code__iexact=postal_code)

def _filter_by_post_office(self, areas: QuerySet) -> QuerySet:
def _filter_by_postal_code_area(self, areas: QuerySet) -> QuerySet:
postal_code_area = self.request.query_params.get("postalcodearea")
if postal_code_area is None:
return areas
return areas.filter(translations__name__iexact=postal_code_area).distinct()
area_ids = list(
PostalCodeArea.objects.filter(
translations__name__iexact=postal_code_area
).values_list("id", flat=True)
)
return areas.filter(id__in=area_ids)

def _filter_by_post_office(self, areas: QuerySet) -> QuerySet:
post_office = self.request.query_params.get("postoffice")
if post_office is None:
return areas
area_ids = list(
PostalCodeArea.objects.filter(
translations__post_office__iexact=post_office
).values_list("id", flat=True)
)
return areas.filter(id__in=area_ids)


@extend_schema_view(
Expand Down
55 changes: 50 additions & 5 deletions address/locale/fi/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-13 20:28+0300\n"
"POT-Creation-Date: 2026-02-19 11:39+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand All @@ -21,8 +21,53 @@ msgstr ""
msgid "Name"
msgstr "Nimi"

msgid "municipalities"
msgstr "kunnat"
msgid "Id"
msgstr "Tunniste"

msgid "addresses"
msgstr "osoitteet"
msgid "Municipality code"
msgstr "Kuntakoodi"

msgid "Area"
msgstr "Alue"

msgid "Municipality"
msgstr "Kunta"

msgid "Municipalities"
msgstr "Kunnat"

msgid "Street"
msgstr "Katu"

msgid "Streets"
msgstr "Kadut"

msgid "Postal code"
msgstr "Postinumero"

msgid "Post office"
msgstr "Postitoimipaikka"

msgid "Postal code area"
msgstr "Postinumeroalue"

msgid "Postal code areas"
msgstr "Postinumeroalueet"

msgid "Number"
msgstr "Numero"

msgid "Number end"
msgstr "Numeron loppu"

msgid "Letter"
msgstr "Kirjain"

msgid "Location"
msgstr "Sijainti"

msgid "Address"
msgstr "Osoite"

msgid "Addresses"
msgstr "Osoitteet"
Loading
Loading