Procházet zdrojové kódy

feat(server, web)!: Move reverse geocoding settings to the UI (#4222)

* feat: reverse geocoding settings

* chore: open api

* re-init geocoder if precision has been updated

* update docs

* chore: update verbiage

* fix: re-init logic

* fix: reset to default

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Daniel Dietzler před 1 rokem
rodič
revize
9bada51d56
37 změnil soubory, kde provedl 654 přidání a 81 odebrání
  1. 49 0
      cli/src/api/open-api/api.ts
  2. 3 5
      docs/docs/install/environment-variables.md
  3. 6 0
      mobile/openapi/.openapi-generator/FILES
  4. 2 0
      mobile/openapi/README.md
  5. 14 0
      mobile/openapi/doc/CitiesFile.md
  6. 1 0
      mobile/openapi/doc/ServerFeaturesDto.md
  7. 1 0
      mobile/openapi/doc/SystemConfigDto.md
  8. 16 0
      mobile/openapi/doc/SystemConfigReverseGeocodingDto.md
  9. 2 0
      mobile/openapi/lib/api.dart
  10. 4 0
      mobile/openapi/lib/api_client.dart
  11. 3 0
      mobile/openapi/lib/api_helper.dart
  12. 91 0
      mobile/openapi/lib/model/cities_file.dart
  13. 9 1
      mobile/openapi/lib/model/server_features_dto.dart
  14. 9 1
      mobile/openapi/lib/model/system_config_dto.dart
  15. 106 0
      mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart
  16. 21 0
      mobile/openapi/test/cities_file_test.dart
  17. 5 0
      mobile/openapi/test/server_features_dto_test.dart
  18. 5 0
      mobile/openapi/test/system_config_dto_test.dart
  19. 32 0
      mobile/openapi/test/system_config_reverse_geocoding_dto_test.dart
  20. 32 0
      server/immich-openapi-specs.json
  21. 3 1
      server/src/domain/metadata/geocoding.repository.ts
  22. 1 0
      server/src/domain/server-info/server-info.dto.ts
  23. 1 0
      server/src/domain/server-info/server-info.service.spec.ts
  24. 12 0
      server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts
  25. 6 0
      server/src/domain/system-config/dto/system-config.dto.ts
  26. 7 0
      server/src/domain/system-config/system-config.core.ts
  27. 5 0
      server/src/domain/system-config/system-config.service.spec.ts
  28. 14 0
      server/src/infra/entities/system-config.entity.ts
  29. 2 18
      server/src/infra/infra.config.ts
  30. 20 6
      server/src/infra/repositories/geocoding.repository.ts
  31. 19 12
      server/src/microservices/processors/metadata-extraction.processor.ts
  32. 1 0
      server/test/e2e/server-info.e2e-spec.ts
  33. 49 0
      web/src/api/open-api/api.ts
  34. 96 33
      web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte
  35. 3 1
      web/src/lib/components/admin-page/settings/setting-accordion.svelte
  36. 1 0
      web/src/lib/stores/server-config.store.ts
  37. 3 3
      web/src/routes/admin/system-settings/+page.svelte

+ 49 - 0
cli/src/api/open-api/api.ts

@@ -1055,6 +1055,22 @@ export interface CheckExistingAssetsResponseDto {
      */
     'existingIds': Array<string>;
 }
+/**
+ * 
+ * @export
+ * @enum {string}
+ */
+
+export const CitiesFile = {
+    Cities15000: 'cities15000',
+    Cities5000: 'cities5000',
+    Cities1000: 'cities1000',
+    Cities500: 'cities500'
+} as const;
+
+export type CitiesFile = typeof CitiesFile[keyof typeof CitiesFile];
+
+
 /**
  * 
  * @export
@@ -2650,6 +2666,12 @@ export interface ServerFeaturesDto {
      * @memberof ServerFeaturesDto
      */
     'passwordLogin': boolean;
+    /**
+     * 
+     * @type {boolean}
+     * @memberof ServerFeaturesDto
+     */
+    'reverseGeocoding': boolean;
     /**
      * 
      * @type {boolean}
@@ -3093,6 +3115,12 @@ export interface SystemConfigDto {
      * @memberof SystemConfigDto
      */
     'passwordLogin': SystemConfigPasswordLoginDto;
+    /**
+     * 
+     * @type {SystemConfigReverseGeocodingDto}
+     * @memberof SystemConfigDto
+     */
+    'reverseGeocoding': SystemConfigReverseGeocodingDto;
     /**
      * 
      * @type {SystemConfigStorageTemplateDto}
@@ -3438,6 +3466,27 @@ export interface SystemConfigPasswordLoginDto {
      */
     'enabled': boolean;
 }
+/**
+ * 
+ * @export
+ * @interface SystemConfigReverseGeocodingDto
+ */
+export interface SystemConfigReverseGeocodingDto {
+    /**
+     * 
+     * @type {CitiesFile}
+     * @memberof SystemConfigReverseGeocodingDto
+     */
+    'citiesFileOverride': CitiesFile;
+    /**
+     * 
+     * @type {boolean}
+     * @memberof SystemConfigReverseGeocodingDto
+     */
+    'enabled': boolean;
+}
+
+
 /**
  * 
  * @export

+ 3 - 5
docs/docs/install/environment-variables.md

@@ -49,11 +49,9 @@ These environment variables are used by the `docker-compose.yml` file and do **N
 
 ## Geocoding
 
-| Variable                           | Description                         |           Default            | Services      |
-| :--------------------------------- | :---------------------------------- | :--------------------------: | :------------ |
-| `DISABLE_REVERSE_GEOCODING`        | Disable Reverse Geocoding Precision |           `false`            | microservices |
-| `REVERSE_GEOCODING_PRECISION`      | Reverse Geocoding Precision         |             `3`              | microservices |
-| `REVERSE_GEOCODING_DUMP_DIRECTORY` | Reverse Geocoding Dump Directory    | `./.reverse-geocoding-dump/` | microservices |
+| Variable                           | Description                      |           Default            | Services      |
+| :--------------------------------- | :------------------------------- | :--------------------------: | :------------ |
+| `REVERSE_GEOCODING_DUMP_DIRECTORY` | Reverse Geocoding Dump Directory | `./.reverse-geocoding-dump/` | microservices |
 
 ## Ports
 

+ 6 - 0
mobile/openapi/.openapi-generator/FILES

@@ -43,6 +43,7 @@ doc/CheckDuplicateAssetDto.md
 doc/CheckDuplicateAssetResponseDto.md
 doc/CheckExistingAssetsDto.md
 doc/CheckExistingAssetsResponseDto.md
+doc/CitiesFile.md
 doc/ClassificationConfig.md
 doc/Colorspace.md
 doc/CreateAlbumDto.md
@@ -126,6 +127,7 @@ doc/SystemConfigMachineLearningDto.md
 doc/SystemConfigMapDto.md
 doc/SystemConfigOAuthDto.md
 doc/SystemConfigPasswordLoginDto.md
+doc/SystemConfigReverseGeocodingDto.md
 doc/SystemConfigStorageTemplateDto.md
 doc/SystemConfigTemplateStorageOptionDto.md
 doc/SystemConfigThumbnailDto.md
@@ -207,6 +209,7 @@ lib/model/check_duplicate_asset_dto.dart
 lib/model/check_duplicate_asset_response_dto.dart
 lib/model/check_existing_assets_dto.dart
 lib/model/check_existing_assets_response_dto.dart
+lib/model/cities_file.dart
 lib/model/classification_config.dart
 lib/model/clip_config.dart
 lib/model/clip_mode.dart
@@ -284,6 +287,7 @@ lib/model/system_config_machine_learning_dto.dart
 lib/model/system_config_map_dto.dart
 lib/model/system_config_o_auth_dto.dart
 lib/model/system_config_password_login_dto.dart
+lib/model/system_config_reverse_geocoding_dto.dart
 lib/model/system_config_storage_template_dto.dart
 lib/model/system_config_template_storage_option_dto.dart
 lib/model/system_config_thumbnail_dto.dart
@@ -343,6 +347,7 @@ test/check_duplicate_asset_dto_test.dart
 test/check_duplicate_asset_response_dto_test.dart
 test/check_existing_assets_dto_test.dart
 test/check_existing_assets_response_dto_test.dart
+test/cities_file_test.dart
 test/classification_config_test.dart
 test/clip_config_test.dart
 test/clip_mode_test.dart
@@ -429,6 +434,7 @@ test/system_config_machine_learning_dto_test.dart
 test/system_config_map_dto_test.dart
 test/system_config_o_auth_dto_test.dart
 test/system_config_password_login_dto_test.dart
+test/system_config_reverse_geocoding_dto_test.dart
 test/system_config_storage_template_dto_test.dart
 test/system_config_template_storage_option_dto_test.dart
 test/system_config_thumbnail_dto_test.dart

+ 2 - 0
mobile/openapi/README.md

@@ -227,6 +227,7 @@ Class | Method | HTTP request | Description
  - [CheckDuplicateAssetResponseDto](doc//CheckDuplicateAssetResponseDto.md)
  - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md)
  - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md)
+ - [CitiesFile](doc//CitiesFile.md)
  - [ClassificationConfig](doc//ClassificationConfig.md)
  - [Colorspace](doc//Colorspace.md)
  - [CreateAlbumDto](doc//CreateAlbumDto.md)
@@ -301,6 +302,7 @@ Class | Method | HTTP request | Description
  - [SystemConfigMapDto](doc//SystemConfigMapDto.md)
  - [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md)
  - [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md)
+ - [SystemConfigReverseGeocodingDto](doc//SystemConfigReverseGeocodingDto.md)
  - [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md)
  - [SystemConfigTemplateStorageOptionDto](doc//SystemConfigTemplateStorageOptionDto.md)
  - [SystemConfigThumbnailDto](doc//SystemConfigThumbnailDto.md)

+ 14 - 0
mobile/openapi/doc/CitiesFile.md

@@ -0,0 +1,14 @@
+# openapi.model.CitiesFile
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+

+ 1 - 0
mobile/openapi/doc/ServerFeaturesDto.md

@@ -15,6 +15,7 @@ Name | Type | Description | Notes
 **oauth** | **bool** |  | 
 **oauthAutoLaunch** | **bool** |  | 
 **passwordLogin** | **bool** |  | 
+**reverseGeocoding** | **bool** |  | 
 **search** | **bool** |  | 
 **sidecar** | **bool** |  | 
 **tagImage** | **bool** |  | 

+ 1 - 0
mobile/openapi/doc/SystemConfigDto.md

@@ -14,6 +14,7 @@ Name | Type | Description | Notes
 **map** | [**SystemConfigMapDto**](SystemConfigMapDto.md) |  | 
 **oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) |  | 
 **passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.md) |  | 
+**reverseGeocoding** | [**SystemConfigReverseGeocodingDto**](SystemConfigReverseGeocodingDto.md) |  | 
 **storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) |  | 
 **thumbnail** | [**SystemConfigThumbnailDto**](SystemConfigThumbnailDto.md) |  | 
 

+ 16 - 0
mobile/openapi/doc/SystemConfigReverseGeocodingDto.md

@@ -0,0 +1,16 @@
+# openapi.model.SystemConfigReverseGeocodingDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**citiesFileOverride** | [**CitiesFile**](CitiesFile.md) |  | 
+**enabled** | **bool** |  | 
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+

+ 2 - 0
mobile/openapi/lib/api.dart

@@ -80,6 +80,7 @@ part 'model/check_duplicate_asset_dto.dart';
 part 'model/check_duplicate_asset_response_dto.dart';
 part 'model/check_existing_assets_dto.dart';
 part 'model/check_existing_assets_response_dto.dart';
+part 'model/cities_file.dart';
 part 'model/classification_config.dart';
 part 'model/colorspace.dart';
 part 'model/create_album_dto.dart';
@@ -154,6 +155,7 @@ part 'model/system_config_machine_learning_dto.dart';
 part 'model/system_config_map_dto.dart';
 part 'model/system_config_o_auth_dto.dart';
 part 'model/system_config_password_login_dto.dart';
+part 'model/system_config_reverse_geocoding_dto.dart';
 part 'model/system_config_storage_template_dto.dart';
 part 'model/system_config_template_storage_option_dto.dart';
 part 'model/system_config_thumbnail_dto.dart';

+ 4 - 0
mobile/openapi/lib/api_client.dart

@@ -251,6 +251,8 @@ class ApiClient {
           return CheckExistingAssetsDto.fromJson(value);
         case 'CheckExistingAssetsResponseDto':
           return CheckExistingAssetsResponseDto.fromJson(value);
+        case 'CitiesFile':
+          return CitiesFileTypeTransformer().decode(value);
         case 'ClassificationConfig':
           return ClassificationConfig.fromJson(value);
         case 'Colorspace':
@@ -399,6 +401,8 @@ class ApiClient {
           return SystemConfigOAuthDto.fromJson(value);
         case 'SystemConfigPasswordLoginDto':
           return SystemConfigPasswordLoginDto.fromJson(value);
+        case 'SystemConfigReverseGeocodingDto':
+          return SystemConfigReverseGeocodingDto.fromJson(value);
         case 'SystemConfigStorageTemplateDto':
           return SystemConfigStorageTemplateDto.fromJson(value);
         case 'SystemConfigTemplateStorageOptionDto':

+ 3 - 0
mobile/openapi/lib/api_helper.dart

@@ -70,6 +70,9 @@ String parameterToString(dynamic value) {
   if (value is CQMode) {
     return CQModeTypeTransformer().encode(value).toString();
   }
+  if (value is CitiesFile) {
+    return CitiesFileTypeTransformer().encode(value).toString();
+  }
   if (value is Colorspace) {
     return ColorspaceTypeTransformer().encode(value).toString();
   }

+ 91 - 0
mobile/openapi/lib/model/cities_file.dart

@@ -0,0 +1,91 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+
+class CitiesFile {
+  /// Instantiate a new enum with the provided [value].
+  const CitiesFile._(this.value);
+
+  /// The underlying value of this enum member.
+  final String value;
+
+  @override
+  String toString() => value;
+
+  String toJson() => value;
+
+  static const cities15000 = CitiesFile._(r'cities15000');
+  static const cities5000 = CitiesFile._(r'cities5000');
+  static const cities1000 = CitiesFile._(r'cities1000');
+  static const cities500 = CitiesFile._(r'cities500');
+
+  /// List of all possible values in this [enum][CitiesFile].
+  static const values = <CitiesFile>[
+    cities15000,
+    cities5000,
+    cities1000,
+    cities500,
+  ];
+
+  static CitiesFile? fromJson(dynamic value) => CitiesFileTypeTransformer().decode(value);
+
+  static List<CitiesFile>? listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <CitiesFile>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = CitiesFile.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+}
+
+/// Transformation class that can [encode] an instance of [CitiesFile] to String,
+/// and [decode] dynamic data back to [CitiesFile].
+class CitiesFileTypeTransformer {
+  factory CitiesFileTypeTransformer() => _instance ??= const CitiesFileTypeTransformer._();
+
+  const CitiesFileTypeTransformer._();
+
+  String encode(CitiesFile data) => data.value;
+
+  /// Decodes a [dynamic value][data] to a CitiesFile.
+  ///
+  /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
+  /// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
+  /// cannot be decoded successfully, then an [UnimplementedError] is thrown.
+  ///
+  /// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
+  /// and users are still using an old app with the old code.
+  CitiesFile? decode(dynamic data, {bool allowNull = true}) {
+    if (data != null) {
+      switch (data) {
+        case r'cities15000': return CitiesFile.cities15000;
+        case r'cities5000': return CitiesFile.cities5000;
+        case r'cities1000': return CitiesFile.cities1000;
+        case r'cities500': return CitiesFile.cities500;
+        default:
+          if (!allowNull) {
+            throw ArgumentError('Unknown enum value to decode: $data');
+          }
+      }
+    }
+    return null;
+  }
+
+  /// Singleton [CitiesFileTypeTransformer] instance.
+  static CitiesFileTypeTransformer? _instance;
+}
+

+ 9 - 1
mobile/openapi/lib/model/server_features_dto.dart

@@ -20,6 +20,7 @@ class ServerFeaturesDto {
     required this.oauth,
     required this.oauthAutoLaunch,
     required this.passwordLogin,
+    required this.reverseGeocoding,
     required this.search,
     required this.sidecar,
     required this.tagImage,
@@ -39,6 +40,8 @@ class ServerFeaturesDto {
 
   bool passwordLogin;
 
+  bool reverseGeocoding;
+
   bool search;
 
   bool sidecar;
@@ -54,6 +57,7 @@ class ServerFeaturesDto {
      other.oauth == oauth &&
      other.oauthAutoLaunch == oauthAutoLaunch &&
      other.passwordLogin == passwordLogin &&
+     other.reverseGeocoding == reverseGeocoding &&
      other.search == search &&
      other.sidecar == sidecar &&
      other.tagImage == tagImage;
@@ -68,12 +72,13 @@ class ServerFeaturesDto {
     (oauth.hashCode) +
     (oauthAutoLaunch.hashCode) +
     (passwordLogin.hashCode) +
+    (reverseGeocoding.hashCode) +
     (search.hashCode) +
     (sidecar.hashCode) +
     (tagImage.hashCode);
 
   @override
-  String toString() => 'ServerFeaturesDto[clipEncode=$clipEncode, configFile=$configFile, facialRecognition=$facialRecognition, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, search=$search, sidecar=$sidecar, tagImage=$tagImage]';
+  String toString() => 'ServerFeaturesDto[clipEncode=$clipEncode, configFile=$configFile, facialRecognition=$facialRecognition, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, tagImage=$tagImage]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -84,6 +89,7 @@ class ServerFeaturesDto {
       json[r'oauth'] = this.oauth;
       json[r'oauthAutoLaunch'] = this.oauthAutoLaunch;
       json[r'passwordLogin'] = this.passwordLogin;
+      json[r'reverseGeocoding'] = this.reverseGeocoding;
       json[r'search'] = this.search;
       json[r'sidecar'] = this.sidecar;
       json[r'tagImage'] = this.tagImage;
@@ -105,6 +111,7 @@ class ServerFeaturesDto {
         oauth: mapValueOfType<bool>(json, r'oauth')!,
         oauthAutoLaunch: mapValueOfType<bool>(json, r'oauthAutoLaunch')!,
         passwordLogin: mapValueOfType<bool>(json, r'passwordLogin')!,
+        reverseGeocoding: mapValueOfType<bool>(json, r'reverseGeocoding')!,
         search: mapValueOfType<bool>(json, r'search')!,
         sidecar: mapValueOfType<bool>(json, r'sidecar')!,
         tagImage: mapValueOfType<bool>(json, r'tagImage')!,
@@ -162,6 +169,7 @@ class ServerFeaturesDto {
     'oauth',
     'oauthAutoLaunch',
     'passwordLogin',
+    'reverseGeocoding',
     'search',
     'sidecar',
     'tagImage',

+ 9 - 1
mobile/openapi/lib/model/system_config_dto.dart

@@ -19,6 +19,7 @@ class SystemConfigDto {
     required this.map,
     required this.oauth,
     required this.passwordLogin,
+    required this.reverseGeocoding,
     required this.storageTemplate,
     required this.thumbnail,
   });
@@ -35,6 +36,8 @@ class SystemConfigDto {
 
   SystemConfigPasswordLoginDto passwordLogin;
 
+  SystemConfigReverseGeocodingDto reverseGeocoding;
+
   SystemConfigStorageTemplateDto storageTemplate;
 
   SystemConfigThumbnailDto thumbnail;
@@ -47,6 +50,7 @@ class SystemConfigDto {
      other.map == map &&
      other.oauth == oauth &&
      other.passwordLogin == passwordLogin &&
+     other.reverseGeocoding == reverseGeocoding &&
      other.storageTemplate == storageTemplate &&
      other.thumbnail == thumbnail;
 
@@ -59,11 +63,12 @@ class SystemConfigDto {
     (map.hashCode) +
     (oauth.hashCode) +
     (passwordLogin.hashCode) +
+    (reverseGeocoding.hashCode) +
     (storageTemplate.hashCode) +
     (thumbnail.hashCode);
 
   @override
-  String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, machineLearning=$machineLearning, map=$map, oauth=$oauth, passwordLogin=$passwordLogin, storageTemplate=$storageTemplate, thumbnail=$thumbnail]';
+  String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, machineLearning=$machineLearning, map=$map, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, storageTemplate=$storageTemplate, thumbnail=$thumbnail]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -73,6 +78,7 @@ class SystemConfigDto {
       json[r'map'] = this.map;
       json[r'oauth'] = this.oauth;
       json[r'passwordLogin'] = this.passwordLogin;
+      json[r'reverseGeocoding'] = this.reverseGeocoding;
       json[r'storageTemplate'] = this.storageTemplate;
       json[r'thumbnail'] = this.thumbnail;
     return json;
@@ -92,6 +98,7 @@ class SystemConfigDto {
         map: SystemConfigMapDto.fromJson(json[r'map'])!,
         oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!,
         passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!,
+        reverseGeocoding: SystemConfigReverseGeocodingDto.fromJson(json[r'reverseGeocoding'])!,
         storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!,
         thumbnail: SystemConfigThumbnailDto.fromJson(json[r'thumbnail'])!,
       );
@@ -147,6 +154,7 @@ class SystemConfigDto {
     'map',
     'oauth',
     'passwordLogin',
+    'reverseGeocoding',
     'storageTemplate',
     'thumbnail',
   };

+ 106 - 0
mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart

@@ -0,0 +1,106 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+class SystemConfigReverseGeocodingDto {
+  /// Returns a new [SystemConfigReverseGeocodingDto] instance.
+  SystemConfigReverseGeocodingDto({
+    required this.citiesFileOverride,
+    required this.enabled,
+  });
+
+  CitiesFile citiesFileOverride;
+
+  bool enabled;
+
+  @override
+  bool operator ==(Object other) => identical(this, other) || other is SystemConfigReverseGeocodingDto &&
+     other.citiesFileOverride == citiesFileOverride &&
+     other.enabled == enabled;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (citiesFileOverride.hashCode) +
+    (enabled.hashCode);
+
+  @override
+  String toString() => 'SystemConfigReverseGeocodingDto[citiesFileOverride=$citiesFileOverride, enabled=$enabled]';
+
+  Map<String, dynamic> toJson() {
+    final json = <String, dynamic>{};
+      json[r'citiesFileOverride'] = this.citiesFileOverride;
+      json[r'enabled'] = this.enabled;
+    return json;
+  }
+
+  /// Returns a new [SystemConfigReverseGeocodingDto] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static SystemConfigReverseGeocodingDto? fromJson(dynamic value) {
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      return SystemConfigReverseGeocodingDto(
+        citiesFileOverride: CitiesFile.fromJson(json[r'citiesFileOverride'])!,
+        enabled: mapValueOfType<bool>(json, r'enabled')!,
+      );
+    }
+    return null;
+  }
+
+  static List<SystemConfigReverseGeocodingDto> listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <SystemConfigReverseGeocodingDto>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = SystemConfigReverseGeocodingDto.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, SystemConfigReverseGeocodingDto> mapFromJson(dynamic json) {
+    final map = <String, SystemConfigReverseGeocodingDto>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = SystemConfigReverseGeocodingDto.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of SystemConfigReverseGeocodingDto-objects as value to a dart map
+  static Map<String, List<SystemConfigReverseGeocodingDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<SystemConfigReverseGeocodingDto>>{};
+    if (json is Map && json.isNotEmpty) {
+      // ignore: parameter_assignments
+      json = json.cast<String, dynamic>();
+      for (final entry in json.entries) {
+        map[entry.key] = SystemConfigReverseGeocodingDto.listFromJson(entry.value, growable: growable,);
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+    'citiesFileOverride',
+    'enabled',
+  };
+}
+

+ 21 - 0
mobile/openapi/test/cities_file_test.dart

@@ -0,0 +1,21 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+import 'package:openapi/api.dart';
+import 'package:test/test.dart';
+
+// tests for CitiesFile
+void main() {
+
+  group('test CitiesFile', () {
+
+  });
+
+}

+ 5 - 0
mobile/openapi/test/server_features_dto_test.dart

@@ -51,6 +51,11 @@ void main() {
       // TODO
     });
 
+    // bool reverseGeocoding
+    test('to test the property `reverseGeocoding`', () async {
+      // TODO
+    });
+
     // bool search
     test('to test the property `search`', () async {
       // TODO

+ 5 - 0
mobile/openapi/test/system_config_dto_test.dart

@@ -46,6 +46,11 @@ void main() {
       // TODO
     });
 
+    // SystemConfigReverseGeocodingDto reverseGeocoding
+    test('to test the property `reverseGeocoding`', () async {
+      // TODO
+    });
+
     // SystemConfigStorageTemplateDto storageTemplate
     test('to test the property `storageTemplate`', () async {
       // TODO

+ 32 - 0
mobile/openapi/test/system_config_reverse_geocoding_dto_test.dart

@@ -0,0 +1,32 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+import 'package:openapi/api.dart';
+import 'package:test/test.dart';
+
+// tests for SystemConfigReverseGeocodingDto
+void main() {
+  // final instance = SystemConfigReverseGeocodingDto();
+
+  group('test SystemConfigReverseGeocodingDto', () {
+    // CitiesFile citiesFileOverride
+    test('to test the property `citiesFileOverride`', () async {
+      // TODO
+    });
+
+    // bool enabled
+    test('to test the property `enabled`', () async {
+      // TODO
+    });
+
+
+  });
+
+}

+ 32 - 0
server/immich-openapi-specs.json

@@ -5914,6 +5914,15 @@
         ],
         "type": "object"
       },
+      "CitiesFile": {
+        "enum": [
+          "cities15000",
+          "cities5000",
+          "cities1000",
+          "cities500"
+        ],
+        "type": "string"
+      },
       "ClassificationConfig": {
         "properties": {
           "enabled": {
@@ -7229,6 +7238,9 @@
           "passwordLogin": {
             "type": "boolean"
           },
+          "reverseGeocoding": {
+            "type": "boolean"
+          },
           "search": {
             "type": "boolean"
           },
@@ -7244,6 +7256,7 @@
           "configFile",
           "facialRecognition",
           "map",
+          "reverseGeocoding",
           "oauth",
           "oauthAutoLaunch",
           "passwordLogin",
@@ -7590,6 +7603,9 @@
           "passwordLogin": {
             "$ref": "#/components/schemas/SystemConfigPasswordLoginDto"
           },
+          "reverseGeocoding": {
+            "$ref": "#/components/schemas/SystemConfigReverseGeocodingDto"
+          },
           "storageTemplate": {
             "$ref": "#/components/schemas/SystemConfigStorageTemplateDto"
           },
@@ -7603,6 +7619,7 @@
           "map",
           "oauth",
           "passwordLogin",
+          "reverseGeocoding",
           "storageTemplate",
           "job",
           "thumbnail"
@@ -7843,6 +7860,21 @@
         ],
         "type": "object"
       },
+      "SystemConfigReverseGeocodingDto": {
+        "properties": {
+          "citiesFileOverride": {
+            "$ref": "#/components/schemas/CitiesFile"
+          },
+          "enabled": {
+            "type": "boolean"
+          }
+        },
+        "required": [
+          "citiesFileOverride",
+          "enabled"
+        ],
+        "type": "object"
+      },
       "SystemConfigStorageTemplateDto": {
         "properties": {
           "template": {

+ 3 - 1
server/src/domain/metadata/geocoding.repository.ts

@@ -1,3 +1,5 @@
+import { InitOptions } from 'local-reverse-geocoder';
+
 export const IGeocodingRepository = 'IGeocodingRepository';
 
 export interface GeoPoint {
@@ -12,7 +14,7 @@ export interface ReverseGeocodeResult {
 }
 
 export interface IGeocodingRepository {
-  init(): Promise<void>;
+  init(options: Partial<InitOptions>): Promise<void>;
   reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
   deleteCache(): Promise<void>;
 }

+ 1 - 0
server/src/domain/server-info/server-info.dto.ts

@@ -90,6 +90,7 @@ export class ServerFeaturesDto implements FeatureFlags {
   configFile!: boolean;
   facialRecognition!: boolean;
   map!: boolean;
+  reverseGeocoding!: boolean;
   oauth!: boolean;
   oauthAutoLaunch!: boolean;
   passwordLogin!: boolean;

+ 1 - 0
server/src/domain/server-info/server-info.service.spec.ts

@@ -151,6 +151,7 @@ describe(ServerInfoService.name, () => {
         clipEncode: true,
         facialRecognition: true,
         map: true,
+        reverseGeocoding: true,
         oauth: false,
         oauthAutoLaunch: false,
         passwordLogin: true,

+ 12 - 0
server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts

@@ -0,0 +1,12 @@
+import { CitiesFile } from '@app/infra/entities';
+import { ApiProperty } from '@nestjs/swagger';
+import { IsBoolean, IsEnum } from 'class-validator';
+
+export class SystemConfigReverseGeocodingDto {
+  @IsBoolean()
+  enabled!: boolean;
+
+  @IsEnum(CitiesFile)
+  @ApiProperty({ enum: CitiesFile, enumName: 'CitiesFile' })
+  citiesFileOverride!: CitiesFile;
+}

+ 6 - 0
server/src/domain/system-config/dto/system-config.dto.ts

@@ -8,6 +8,7 @@ import { SystemConfigMachineLearningDto } from './system-config-machine-learning
 import { SystemConfigMapDto } from './system-config-map.dto';
 import { SystemConfigOAuthDto } from './system-config-oauth.dto';
 import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto';
+import { SystemConfigReverseGeocodingDto } from './system-config-reverse-geocoding.dto';
 import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto';
 
 export class SystemConfigDto implements SystemConfig {
@@ -36,6 +37,11 @@ export class SystemConfigDto implements SystemConfig {
   @IsObject()
   passwordLogin!: SystemConfigPasswordLoginDto;
 
+  @Type(() => SystemConfigReverseGeocodingDto)
+  @ValidateNested()
+  @IsObject()
+  reverseGeocoding!: SystemConfigReverseGeocodingDto;
+
   @Type(() => SystemConfigStorageTemplateDto)
   @ValidateNested()
   @IsObject()

+ 7 - 0
server/src/domain/system-config/system-config.core.ts

@@ -1,6 +1,7 @@
 import {
   AudioCodec,
   CQMode,
+  CitiesFile,
   Colorspace,
   SystemConfig,
   SystemConfigEntity,
@@ -81,6 +82,10 @@ export const defaults = Object.freeze<SystemConfig>({
     enabled: true,
     tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
   },
+  reverseGeocoding: {
+    enabled: true,
+    citiesFileOverride: CitiesFile.CITIES_500,
+  },
   oauth: {
     enabled: false,
     issuerUrl: '',
@@ -115,6 +120,7 @@ export enum FeatureFlag {
   FACIAL_RECOGNITION = 'facialRecognition',
   TAG_IMAGE = 'tagImage',
   MAP = 'map',
+  REVERSE_GEOCODING = 'reverseGeocoding',
   SIDECAR = 'sidecar',
   SEARCH = 'search',
   OAUTH = 'oauth',
@@ -177,6 +183,7 @@ export class SystemConfigCore {
       [FeatureFlag.FACIAL_RECOGNITION]: mlEnabled && config.machineLearning.facialRecognition.enabled,
       [FeatureFlag.TAG_IMAGE]: mlEnabled && config.machineLearning.classification.enabled,
       [FeatureFlag.MAP]: config.map.enabled,
+      [FeatureFlag.REVERSE_GEOCODING]: config.reverseGeocoding.enabled,
       [FeatureFlag.SIDECAR]: true,
       [FeatureFlag.SEARCH]: process.env.TYPESENSE_ENABLED !== 'false',
 

+ 5 - 0
server/src/domain/system-config/system-config.service.spec.ts

@@ -1,6 +1,7 @@
 import {
   AudioCodec,
   CQMode,
+  CitiesFile,
   Colorspace,
   SystemConfig,
   SystemConfigEntity,
@@ -80,6 +81,10 @@ const updatedConfig = Object.freeze<SystemConfig>({
     enabled: true,
     tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
   },
+  reverseGeocoding: {
+    enabled: true,
+    citiesFileOverride: CitiesFile.CITIES_500,
+  },
   oauth: {
     autoLaunch: true,
     autoRegister: true,

+ 14 - 0
server/src/infra/entities/system-config.entity.ts

@@ -64,6 +64,9 @@ export enum SystemConfigKey {
   MAP_ENABLED = 'map.enabled',
   MAP_TILE_URL = 'map.tileUrl',
 
+  REVERSE_GEOCODING_ENABLED = 'reverseGeocoding.enabled',
+  REVERSE_GEOCODING_CITIES_FILE_OVERRIDE = 'reverseGeocoding.citiesFileOverride',
+
   OAUTH_ENABLED = 'oauth.enabled',
   OAUTH_ISSUER_URL = 'oauth.issuerUrl',
   OAUTH_CLIENT_ID = 'oauth.clientId',
@@ -130,6 +133,13 @@ export enum Colorspace {
   P3 = 'p3',
 }
 
+export enum CitiesFile {
+  CITIES_15000 = 'cities15000',
+  CITIES_5000 = 'cities5000',
+  CITIES_1000 = 'cities1000',
+  CITIES_500 = 'cities500',
+}
+
 export interface SystemConfig {
   ffmpeg: {
     crf: number;
@@ -175,6 +185,10 @@ export interface SystemConfig {
     enabled: boolean;
     tileUrl: string;
   };
+  reverseGeocoding: {
+    enabled: boolean;
+    citiesFileOverride: CitiesFile;
+  };
   oauth: {
     enabled: boolean;
     issuerUrl: string;

+ 2 - 18
server/src/infra/infra.config.ts

@@ -2,7 +2,6 @@ import { QueueName } from '@app/domain';
 import { RegisterQueueOptions } from '@nestjs/bullmq';
 import { QueueOptions } from 'bullmq';
 import { RedisOptions } from 'ioredis';
-import { InitOptions } from 'local-reverse-geocoder';
 import { ConfigurationOptions } from 'typesense/lib/Typesense/Configuration';
 
 function parseRedisConfig(): RedisOptions {
@@ -72,20 +71,5 @@ function parseTypeSenseConfig(): ConfigurationOptions {
 
 export const typesenseConfig: ConfigurationOptions = parseTypeSenseConfig();
 
-function parseLocalGeocodingConfig(): InitOptions {
-  const precision = Number(process.env.REVERSE_GEOCODING_PRECISION);
-
-  return {
-    citiesFileOverride: precision ? ['cities15000', 'cities5000', 'cities1000', 'cities500'][precision] : undefined,
-    load: {
-      admin1: true,
-      admin2: true,
-      admin3And4: false,
-      alternateNames: false,
-    },
-    countries: [],
-    dumpDirectory: process.env.REVERSE_GEOCODING_DUMP_DIRECTORY || process.cwd() + '/.reverse-geocoding-dump/',
-  };
-}
-
-export const localGeocodingConfig: InitOptions = parseLocalGeocodingConfig();
+export const REVERSE_GEOCODING_DUMP_DIRECTORY =
+  process.env.REVERSE_GEOCODING_DUMP_DIRECTORY || process.cwd() + '/.reverse-geocoding-dump/';

+ 20 - 6
server/src/infra/repositories/geocoding.repository.ts

@@ -1,9 +1,9 @@
 import { GeoPoint, IGeocodingRepository, ReverseGeocodeResult } from '@app/domain';
-import { localGeocodingConfig } from '@app/infra';
+import { REVERSE_GEOCODING_DUMP_DIRECTORY } from '@app/infra';
 import { Injectable, Logger } from '@nestjs/common';
 import { readdir, rm } from 'fs/promises';
 import { getName } from 'i18n-iso-countries';
-import geocoder, { AddressObject } from 'local-reverse-geocoder';
+import geocoder, { AddressObject, InitOptions } from 'local-reverse-geocoder';
 import path from 'path';
 import { promisify } from 'util';
 
@@ -18,19 +18,33 @@ export type GeoData = AddressObject & {
   admin2Code?: AdminCode | string;
 };
 
-const init = (): Promise<void> => new Promise<void>((resolve) => geocoder.init(localGeocodingConfig, resolve));
 const lookup = promisify<GeoPoint[], number, AddressObject[][]>(geocoder.lookUp).bind(geocoder);
 
 @Injectable()
 export class GeocodingRepository implements IGeocodingRepository {
   private logger = new Logger(GeocodingRepository.name);
 
-  async init(): Promise<void> {
-    await init();
+  async init(options: Partial<InitOptions>): Promise<void> {
+    return new Promise<void>((resolve) => {
+      geocoder.init(
+        {
+          load: {
+            admin1: true,
+            admin2: true,
+            admin3And4: false,
+            alternateNames: false,
+          },
+          countries: [],
+          dumpDirectory: REVERSE_GEOCODING_DUMP_DIRECTORY,
+          ...options,
+        },
+        resolve,
+      );
+    });
   }
 
   async deleteCache() {
-    const dumpDirectory = localGeocodingConfig.dumpDirectory;
+    const dumpDirectory = REVERSE_GEOCODING_DUMP_DIRECTORY;
     if (dumpDirectory) {
       // delete contents
       const items = await readdir(dumpDirectory, { withFileTypes: true });

+ 19 - 12
server/src/microservices/processors/metadata-extraction.processor.ts

@@ -1,4 +1,5 @@
 import {
+  FeatureFlag,
   IAlbumRepository,
   IAssetRepository,
   IBaseJob,
@@ -7,17 +8,18 @@ import {
   IGeocodingRepository,
   IJobRepository,
   IStorageRepository,
+  ISystemConfigRepository,
   JobName,
   JOBS_ASSET_PAGINATION_SIZE,
   QueueName,
   StorageCore,
   StorageFolder,
+  SystemConfigCore,
   usePagination,
   WithoutProperty,
 } from '@app/domain';
 import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
 import { Inject, Logger } from '@nestjs/common';
-import { ConfigService } from '@nestjs/config';
 import { DefaultReadTaskOptions, ExifDateTime, exiftool, ReadTaskOptions, Tags } from 'exiftool-vendored';
 import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
 import * as geotz from 'geo-tz';
@@ -51,8 +53,9 @@ const validate = <T>(value: T): T | null => (typeof value === 'string' ? null :
 
 export class MetadataExtractionProcessor {
   private logger = new Logger(MetadataExtractionProcessor.name);
-  private reverseGeocodingEnabled: boolean;
   private storageCore: StorageCore;
+  private configCore: SystemConfigCore;
+  private oldCities?: string;
 
   constructor(
     @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@@ -61,31 +64,35 @@ export class MetadataExtractionProcessor {
     @Inject(IGeocodingRepository) private geocodingRepository: IGeocodingRepository,
     @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
     @Inject(IStorageRepository) private storageRepository: IStorageRepository,
-
-    configService: ConfigService,
+    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
   ) {
-    this.reverseGeocodingEnabled = !configService.get('DISABLE_REVERSE_GEOCODING');
     this.storageCore = new StorageCore(storageRepository);
+    this.configCore = new SystemConfigCore(configRepository);
+    this.configCore.config$.subscribe(() => this.init());
   }
 
   async init(deleteCache = false) {
-    this.logger.log(`Reverse geocoding is ${this.reverseGeocodingEnabled ? 'enabled' : 'disabled'}`);
-    if (!this.reverseGeocodingEnabled) {
+    const { reverseGeocoding } = await this.configCore.getConfig();
+    const { citiesFileOverride } = reverseGeocoding;
+
+    if (!reverseGeocoding.enabled) {
       return;
     }
 
     try {
       if (deleteCache) {
         await this.geocodingRepository.deleteCache();
+      } else if (this.oldCities && this.oldCities === citiesFileOverride) {
+        return;
       }
-      this.logger.log('Initializing Reverse Geocoding');
 
       await this.jobRepository.pause(QueueName.METADATA_EXTRACTION);
-      await this.geocodingRepository.init();
+      await this.geocodingRepository.init({ citiesFileOverride });
       await this.jobRepository.resume(QueueName.METADATA_EXTRACTION);
 
-      this.logger.log('Reverse Geocoding Initialized');
-    } catch (error: any) {
+      this.logger.log(`Initialized local reverse geocoder with ${citiesFileOverride}`);
+      this.oldCities = citiesFileOverride;
+    } catch (error: Error | any) {
       this.logger.error(`Unable to initialize reverse geocoding: ${error}`, error?.stack);
     }
   }
@@ -161,7 +168,7 @@ export class MetadataExtractionProcessor {
 
   private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntity) {
     const { latitude, longitude } = exifData;
-    if (!this.reverseGeocodingEnabled || !longitude || !latitude) {
+    if (!(await this.configCore.hasFeature(FeatureFlag.REVERSE_GEOCODING)) || !longitude || !latitude) {
       return;
     }
 

+ 1 - 0
server/test/e2e/server-info.e2e-spec.ts

@@ -85,6 +85,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
         configFile: false,
         facialRecognition: true,
         map: true,
+        reverseGeocoding: true,
         oauth: false,
         oauthAutoLaunch: false,
         passwordLogin: true,

+ 49 - 0
web/src/api/open-api/api.ts

@@ -1055,6 +1055,22 @@ export interface CheckExistingAssetsResponseDto {
      */
     'existingIds': Array<string>;
 }
+/**
+ * 
+ * @export
+ * @enum {string}
+ */
+
+export const CitiesFile = {
+    Cities15000: 'cities15000',
+    Cities5000: 'cities5000',
+    Cities1000: 'cities1000',
+    Cities500: 'cities500'
+} as const;
+
+export type CitiesFile = typeof CitiesFile[keyof typeof CitiesFile];
+
+
 /**
  * 
  * @export
@@ -2650,6 +2666,12 @@ export interface ServerFeaturesDto {
      * @memberof ServerFeaturesDto
      */
     'passwordLogin': boolean;
+    /**
+     * 
+     * @type {boolean}
+     * @memberof ServerFeaturesDto
+     */
+    'reverseGeocoding': boolean;
     /**
      * 
      * @type {boolean}
@@ -3093,6 +3115,12 @@ export interface SystemConfigDto {
      * @memberof SystemConfigDto
      */
     'passwordLogin': SystemConfigPasswordLoginDto;
+    /**
+     * 
+     * @type {SystemConfigReverseGeocodingDto}
+     * @memberof SystemConfigDto
+     */
+    'reverseGeocoding': SystemConfigReverseGeocodingDto;
     /**
      * 
      * @type {SystemConfigStorageTemplateDto}
@@ -3438,6 +3466,27 @@ export interface SystemConfigPasswordLoginDto {
      */
     'enabled': boolean;
 }
+/**
+ * 
+ * @export
+ * @interface SystemConfigReverseGeocodingDto
+ */
+export interface SystemConfigReverseGeocodingDto {
+    /**
+     * 
+     * @type {CitiesFile}
+     * @memberof SystemConfigReverseGeocodingDto
+     */
+    'citiesFileOverride': CitiesFile;
+    /**
+     * 
+     * @type {boolean}
+     * @memberof SystemConfigReverseGeocodingDto
+     */
+    'enabled': boolean;
+}
+
+
 /**
  * 
  * @export

+ 96 - 33
web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte

@@ -4,23 +4,25 @@
     NotificationType,
   } from '$lib/components/shared-components/notification/notification';
   import { handleError } from '$lib/utils/handle-error';
-  import { api, SystemConfigMapDto } from '@api';
-  import { isEqual } from 'lodash-es';
+  import { api, CitiesFile, SystemConfigDto } from '@api';
+  import { cloneDeep, isEqual } from 'lodash-es';
   import { fade } from 'svelte/transition';
+  import SettingAccordion from '../setting-accordion.svelte';
   import SettingButtonsRow from '../setting-buttons-row.svelte';
-  import SettingSwitch from '../setting-switch.svelte';
   import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
+  import SettingSwitch from '../setting-switch.svelte';
+  import SettingSelect from '../setting-select.svelte';
 
-  export let mapConfig: SystemConfigMapDto; // this is the config that is being edited
+  export let config: SystemConfigDto; // this is the config that is being edited
   export let disabled = false;
 
-  let savedConfig: SystemConfigMapDto;
-  let defaultConfig: SystemConfigMapDto;
+  let savedConfig: SystemConfigDto;
+  let defaultConfig: SystemConfigDto;
 
-  async function getConfigs() {
+  async function refreshConfig() {
     [savedConfig, defaultConfig] = await Promise.all([
-      api.systemConfigApi.getConfig().then((res) => res.data.map),
-      api.systemConfigApi.getDefaults().then((res) => res.data.map),
+      api.systemConfigApi.getConfig().then((res) => res.data),
+      api.systemConfigApi.getDefaults().then((res) => res.data),
     ]);
   }
 
@@ -28,11 +30,21 @@
     try {
       const { data: current } = await api.systemConfigApi.getConfig();
       const { data: updated } = await api.systemConfigApi.updateConfig({
-        systemConfigDto: { ...current, map: mapConfig },
+        systemConfigDto: {
+          ...current,
+          map: {
+            enabled: config.map.enabled,
+            tileUrl: config.map.tileUrl,
+          },
+          reverseGeocoding: {
+            enabled: config.reverseGeocoding.enabled,
+            citiesFileOverride: config.reverseGeocoding.citiesFileOverride,
+          },
+        },
       });
 
-      mapConfig = { ...updated.map };
-      savedConfig = { ...updated.map };
+      config = cloneDeep(updated);
+      savedConfig = cloneDeep(updated);
 
       notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
     } catch (error) {
@@ -43,8 +55,8 @@
   async function reset() {
     const { data: resetConfig } = await api.systemConfigApi.getConfig();
 
-    mapConfig = { ...resetConfig.map };
-    savedConfig = { ...resetConfig.map };
+    config = cloneDeep(resetConfig);
+    savedConfig = cloneDeep(resetConfig);
 
     notificationController.show({
       message: 'Reset settings to the recent saved settings',
@@ -55,8 +67,8 @@
   async function resetToDefault() {
     const { data: configs } = await api.systemConfigApi.getDefaults();
 
-    mapConfig = { ...configs.map };
-    defaultConfig = { ...configs.map };
+    config = cloneDeep(configs);
+    defaultConfig = cloneDeep(configs);
 
     notificationController.show({
       message: 'Reset map settings to default',
@@ -65,30 +77,81 @@
   }
 </script>
 
-<div>
-  {#await getConfigs() then}
+<div class="mt-2">
+  {#await refreshConfig() then}
     <div in:fade={{ duration: 500 }}>
       <form autocomplete="off" on:submit|preventDefault>
-        <div class="ml-4 mt-4 flex flex-col gap-4">
-          <SettingSwitch title="ENABLED" {disabled} subtitle="Enable map features" bind:checked={mapConfig.enabled} />
-
-          <hr />
-
-          <SettingInputField
-            inputType={SettingInputFieldType.TEXT}
-            label="Tile URL"
-            desc="URL to a leaflet compatible tile server"
-            bind:value={mapConfig.tileUrl}
-            required={true}
-            disabled={disabled || !mapConfig.enabled}
-            isEdited={mapConfig.tileUrl !== savedConfig.tileUrl}
-          />
+        <div class="flex flex-col gap-4">
+          <SettingAccordion title="Map Settings" subtitle="Manage map settings">
+            <div class="ml-4 mt-4 flex flex-col gap-4">
+              <SettingSwitch
+                title="ENABLED"
+                {disabled}
+                subtitle="Enable map features"
+                bind:checked={config.map.enabled}
+              />
+
+              <hr />
+
+              <SettingInputField
+                inputType={SettingInputFieldType.TEXT}
+                label="Tile URL"
+                desc="URL to a leaflet compatible tile server"
+                bind:value={config.map.tileUrl}
+                required={true}
+                disabled={disabled || !config.map.enabled}
+                isEdited={config.map.tileUrl !== savedConfig.map.tileUrl}
+              />
+            </div></SettingAccordion
+          >
+
+          <SettingAccordion title="Reverse Geocoding Settings">
+            <svelte:fragment slot="subtitle">
+              <p class="text-sm dark:text-immich-dark-fg">
+                Manage <a
+                  href="https://immich.app/docs/features/reverse-geocoding"
+                  class="underline"
+                  target="_blank"
+                  rel="noreferrer">Reverse Geocoding</a
+                > settings
+              </p>
+            </svelte:fragment>
+            <div class="ml-4 mt-4 flex flex-col gap-4">
+              <SettingSwitch
+                title="ENABLED"
+                {disabled}
+                subtitle="Enable reverse geocoding"
+                bind:checked={config.reverseGeocoding.enabled}
+              />
+
+              <hr />
+
+              <SettingSelect
+                label="Precision"
+                desc="Set reverse geocoding precision"
+                name="reverse-geocoding-precision"
+                bind:value={config.reverseGeocoding.citiesFileOverride}
+                options={[
+                  { value: CitiesFile.Cities500, text: 'Cities with more than 500 people' },
+                  { value: CitiesFile.Cities1000, text: 'Cities with more than 1000 people' },
+                  { value: CitiesFile.Cities5000, text: 'Cities with more than 5000 people' },
+                  { value: CitiesFile.Cities15000, text: 'Cities with more than 15000 people' },
+                ]}
+                disabled={disabled || !config.reverseGeocoding.enabled}
+                isEdited={config.reverseGeocoding.citiesFileOverride !==
+                  savedConfig.reverseGeocoding.citiesFileOverride}
+              />
+            </div></SettingAccordion
+          >
 
           <SettingButtonsRow
             on:reset={reset}
             on:save={saveSetting}
             on:reset-to-default={resetToDefault}
-            showResetToDefault={!isEqual(savedConfig, defaultConfig)}
+            showResetToDefault={!isEqual(
+              { ...savedConfig.map, ...savedConfig.reverseGeocoding },
+              { ...defaultConfig.map, ...defaultConfig.reverseGeocoding },
+            )}
             {disabled}
           />
         </div>

+ 3 - 1
web/src/lib/components/admin-page/settings/setting-accordion.svelte

@@ -14,7 +14,9 @@
         {title}
       </h2>
 
-      <p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
+      <slot name="subtitle">
+        <p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
+      </slot>
     </div>
 
     <button

+ 1 - 0
web/src/lib/stores/server-config.store.ts

@@ -10,6 +10,7 @@ export const featureFlags = writable<FeatureFlags>({
   sidecar: true,
   tagImage: true,
   map: true,
+  reverseGeocoding: true,
   search: true,
   oauth: false,
   oauthAutoLaunch: false,

+ 3 - 3
web/src/routes/admin/system-settings/+page.svelte

@@ -67,12 +67,12 @@
       <JobSettings disabled={$featureFlags.configFile} jobConfig={configs.job} />
     </SettingAccordion>
 
-    <SettingAccordion title="Machine Learning Settings" subtitle="Manage model settings">
+    <SettingAccordion title="Machine Learning Settings" subtitle="Manage machine learning features and settings">
       <MachineLearningSettings disabled={$featureFlags.configFile} machineLearningConfig={configs.machineLearning} />
     </SettingAccordion>
 
-    <SettingAccordion title="Map Settings" subtitle="Manage map settings">
-      <MapSettings disabled={$featureFlags.configFile} mapConfig={configs.map} />
+    <SettingAccordion title="Map & GPS Settings" subtitle="Manage map related features and setting">
+      <MapSettings disabled={$featureFlags.configFile} config={configs} />
     </SettingAccordion>
 
     <SettingAccordion title="OAuth Authentication" subtitle="Manage the login with OAuth settings">