diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index 6fb7478d04..a68b981381 100644
--- a/mobile/openapi/lib/api.dart
+++ b/mobile/openapi/lib/api.dart
@@ -197,12 +197,14 @@ part 'model/ratings_update.dart';
 part 'model/reaction_level.dart';
 part 'model/reaction_type.dart';
 part 'model/reverse_geocoding_state_response_dto.dart';
+part 'model/search_album_name_response_dto.dart';
 part 'model/search_album_response_dto.dart';
 part 'model/search_asset_response_dto.dart';
 part 'model/search_explore_item.dart';
 part 'model/search_explore_response_dto.dart';
 part 'model/search_facet_count_response_dto.dart';
 part 'model/search_facet_response_dto.dart';
+part 'model/search_person_name_response_dto.dart';
 part 'model/search_response_dto.dart';
 part 'model/search_suggestion_type.dart';
 part 'model/server_about_response_dto.dart';
diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart
index 985029f106..c44e51b420 100644
--- a/mobile/openapi/lib/api/search_api.dart
+++ b/mobile/openapi/lib/api/search_api.dart
@@ -193,6 +193,82 @@ class SearchApi {
     return null;
   }
 
+  /// Performs an HTTP 'GET /search/album' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [String] name (required):
+  ///
+  /// * [num] page:
+  ///   Page number for pagination
+  ///
+  /// * [bool] shared:
+  ///   true: only shared albums false: only non-shared own albums undefined: shared and owned albums
+  ///
+  /// * [num] size:
+  ///   Number of items per page
+  Future<Response> searchAlbumWithHttpInfo(String name, { num? page, bool? shared, num? size, }) async {
+    // ignore: prefer_const_declarations
+    final path = r'/search/album';
+
+    // ignore: prefer_final_locals
+    Object? postBody;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+      queryParams.addAll(_queryParams('', 'name', name));
+    if (page != null) {
+      queryParams.addAll(_queryParams('', 'page', page));
+    }
+    if (shared != null) {
+      queryParams.addAll(_queryParams('', 'shared', shared));
+    }
+    if (size != null) {
+      queryParams.addAll(_queryParams('', 'size', size));
+    }
+
+    const contentTypes = <String>[];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'GET',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [String] name (required):
+  ///
+  /// * [num] page:
+  ///   Page number for pagination
+  ///
+  /// * [bool] shared:
+  ///   true: only shared albums false: only non-shared own albums undefined: shared and owned albums
+  ///
+  /// * [num] size:
+  ///   Number of items per page
+  Future<SearchAlbumNameResponseDto?> searchAlbum(String name, { num? page, bool? shared, num? size, }) async {
+    final response = await searchAlbumWithHttpInfo(name,  page: page, shared: shared, size: size, );
+    if (response.statusCode >= HttpStatus.badRequest) {
+      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+    }
+    // When a remote server returns no body with a status of 204, we shall not decode it.
+    // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+    // FormatException when trying to decode an empty string.
+    if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+      return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchAlbumNameResponseDto',) as SearchAlbumNameResponseDto;
+    
+    }
+    return null;
+  }
+
   /// Performs an HTTP 'POST /search/metadata' operation and returns the [Response].
   /// Parameters:
   ///
@@ -245,8 +321,14 @@ class SearchApi {
   ///
   /// * [String] name (required):
   ///
+  /// * [num] page:
+  ///   This property was added in v118.0.0
+  ///
+  /// * [num] size:
+  ///   This property was added in v118.0.0
+  ///
   /// * [bool] withHidden:
-  Future<Response> searchPersonWithHttpInfo(String name, { bool? withHidden, }) async {
+  Future<Response> searchPersonWithHttpInfo(String name, { num? page, num? size, bool? withHidden, }) async {
     // ignore: prefer_const_declarations
     final path = r'/search/person';
 
@@ -258,6 +340,12 @@ class SearchApi {
     final formParams = <String, String>{};
 
       queryParams.addAll(_queryParams('', 'name', name));
+    if (page != null) {
+      queryParams.addAll(_queryParams('', 'page', page));
+    }
+    if (size != null) {
+      queryParams.addAll(_queryParams('', 'size', size));
+    }
     if (withHidden != null) {
       queryParams.addAll(_queryParams('', 'withHidden', withHidden));
     }
@@ -280,9 +368,15 @@ class SearchApi {
   ///
   /// * [String] name (required):
   ///
+  /// * [num] page:
+  ///   This property was added in v118.0.0
+  ///
+  /// * [num] size:
+  ///   This property was added in v118.0.0
+  ///
   /// * [bool] withHidden:
-  Future<List<PersonResponseDto>?> searchPerson(String name, { bool? withHidden, }) async {
-    final response = await searchPersonWithHttpInfo(name,  withHidden: withHidden, );
+  Future<SearchPersonNameResponseDto?> searchPerson(String name, { num? page, num? size, bool? withHidden, }) async {
+    final response = await searchPersonWithHttpInfo(name,  page: page, size: size, withHidden: withHidden, );
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }
@@ -290,11 +384,8 @@ class SearchApi {
     // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
     // FormatException when trying to decode an empty string.
     if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
-      final responseBody = await _decodeBodyBytes(response);
-      return (await apiClient.deserializeAsync(responseBody, 'List<PersonResponseDto>') as List)
-        .cast<PersonResponseDto>()
-        .toList(growable: false);
-
+      return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchPersonNameResponseDto',) as SearchPersonNameResponseDto;
+    
     }
     return null;
   }
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index c1025b0bd4..dde80d4c4f 100644
--- a/mobile/openapi/lib/api_client.dart
+++ b/mobile/openapi/lib/api_client.dart
@@ -448,6 +448,8 @@ class ApiClient {
           return ReactionTypeTypeTransformer().decode(value);
         case 'ReverseGeocodingStateResponseDto':
           return ReverseGeocodingStateResponseDto.fromJson(value);
+        case 'SearchAlbumNameResponseDto':
+          return SearchAlbumNameResponseDto.fromJson(value);
         case 'SearchAlbumResponseDto':
           return SearchAlbumResponseDto.fromJson(value);
         case 'SearchAssetResponseDto':
@@ -460,6 +462,8 @@ class ApiClient {
           return SearchFacetCountResponseDto.fromJson(value);
         case 'SearchFacetResponseDto':
           return SearchFacetResponseDto.fromJson(value);
+        case 'SearchPersonNameResponseDto':
+          return SearchPersonNameResponseDto.fromJson(value);
         case 'SearchResponseDto':
           return SearchResponseDto.fromJson(value);
         case 'SearchSuggestionType':
diff --git a/mobile/openapi/lib/model/search_album_name_response_dto.dart b/mobile/openapi/lib/model/search_album_name_response_dto.dart
new file mode 100644
index 0000000000..62dc2f229a
--- /dev/null
+++ b/mobile/openapi/lib/model/search_album_name_response_dto.dart
@@ -0,0 +1,133 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.18
+
+// 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 SearchAlbumNameResponseDto {
+  /// Returns a new [SearchAlbumNameResponseDto] instance.
+  SearchAlbumNameResponseDto({
+    this.albums = const [],
+    this.hasNextPage,
+    this.total,
+  });
+
+  List<AlbumResponseDto> albums;
+
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  bool? hasNextPage;
+
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  int? total;
+
+  @override
+  bool operator ==(Object other) => identical(this, other) || other is SearchAlbumNameResponseDto &&
+    _deepEquality.equals(other.albums, albums) &&
+    other.hasNextPage == hasNextPage &&
+    other.total == total;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (albums.hashCode) +
+    (hasNextPage == null ? 0 : hasNextPage!.hashCode) +
+    (total == null ? 0 : total!.hashCode);
+
+  @override
+  String toString() => 'SearchAlbumNameResponseDto[albums=$albums, hasNextPage=$hasNextPage, total=$total]';
+
+  Map<String, dynamic> toJson() {
+    final json = <String, dynamic>{};
+      json[r'albums'] = this.albums;
+    if (this.hasNextPage != null) {
+      json[r'hasNextPage'] = this.hasNextPage;
+    } else {
+    //  json[r'hasNextPage'] = null;
+    }
+    if (this.total != null) {
+      json[r'total'] = this.total;
+    } else {
+    //  json[r'total'] = null;
+    }
+    return json;
+  }
+
+  /// Returns a new [SearchAlbumNameResponseDto] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static SearchAlbumNameResponseDto? fromJson(dynamic value) {
+    upgradeDto(value, "SearchAlbumNameResponseDto");
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      return SearchAlbumNameResponseDto(
+        albums: AlbumResponseDto.listFromJson(json[r'albums']),
+        hasNextPage: mapValueOfType<bool>(json, r'hasNextPage'),
+        total: mapValueOfType<int>(json, r'total'),
+      );
+    }
+    return null;
+  }
+
+  static List<SearchAlbumNameResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <SearchAlbumNameResponseDto>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = SearchAlbumNameResponseDto.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, SearchAlbumNameResponseDto> mapFromJson(dynamic json) {
+    final map = <String, SearchAlbumNameResponseDto>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = SearchAlbumNameResponseDto.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of SearchAlbumNameResponseDto-objects as value to a dart map
+  static Map<String, List<SearchAlbumNameResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<SearchAlbumNameResponseDto>>{};
+    if (json is Map && json.isNotEmpty) {
+      // ignore: parameter_assignments
+      json = json.cast<String, dynamic>();
+      for (final entry in json.entries) {
+        map[entry.key] = SearchAlbumNameResponseDto.listFromJson(entry.value, growable: growable,);
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+    'albums',
+  };
+}
+
diff --git a/mobile/openapi/lib/model/search_person_name_response_dto.dart b/mobile/openapi/lib/model/search_person_name_response_dto.dart
new file mode 100644
index 0000000000..67353e9eb2
--- /dev/null
+++ b/mobile/openapi/lib/model/search_person_name_response_dto.dart
@@ -0,0 +1,133 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.18
+
+// 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 SearchPersonNameResponseDto {
+  /// Returns a new [SearchPersonNameResponseDto] instance.
+  SearchPersonNameResponseDto({
+    this.hasNextPage,
+    this.people = const [],
+    this.total,
+  });
+
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  bool? hasNextPage;
+
+  List<PersonResponseDto> people;
+
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  int? total;
+
+  @override
+  bool operator ==(Object other) => identical(this, other) || other is SearchPersonNameResponseDto &&
+    other.hasNextPage == hasNextPage &&
+    _deepEquality.equals(other.people, people) &&
+    other.total == total;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (hasNextPage == null ? 0 : hasNextPage!.hashCode) +
+    (people.hashCode) +
+    (total == null ? 0 : total!.hashCode);
+
+  @override
+  String toString() => 'SearchPersonNameResponseDto[hasNextPage=$hasNextPage, people=$people, total=$total]';
+
+  Map<String, dynamic> toJson() {
+    final json = <String, dynamic>{};
+    if (this.hasNextPage != null) {
+      json[r'hasNextPage'] = this.hasNextPage;
+    } else {
+    //  json[r'hasNextPage'] = null;
+    }
+      json[r'people'] = this.people;
+    if (this.total != null) {
+      json[r'total'] = this.total;
+    } else {
+    //  json[r'total'] = null;
+    }
+    return json;
+  }
+
+  /// Returns a new [SearchPersonNameResponseDto] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static SearchPersonNameResponseDto? fromJson(dynamic value) {
+    upgradeDto(value, "SearchPersonNameResponseDto");
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      return SearchPersonNameResponseDto(
+        hasNextPage: mapValueOfType<bool>(json, r'hasNextPage'),
+        people: PersonResponseDto.listFromJson(json[r'people']),
+        total: mapValueOfType<int>(json, r'total'),
+      );
+    }
+    return null;
+  }
+
+  static List<SearchPersonNameResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <SearchPersonNameResponseDto>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = SearchPersonNameResponseDto.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, SearchPersonNameResponseDto> mapFromJson(dynamic json) {
+    final map = <String, SearchPersonNameResponseDto>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = SearchPersonNameResponseDto.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of SearchPersonNameResponseDto-objects as value to a dart map
+  static Map<String, List<SearchPersonNameResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<SearchPersonNameResponseDto>>{};
+    if (json is Map && json.isNotEmpty) {
+      // ignore: parameter_assignments
+      json = json.cast<String, dynamic>();
+      for (final entry in json.entries) {
+        map[entry.key] = SearchPersonNameResponseDto.listFromJson(entry.value, growable: growable,);
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+    'people',
+  };
+}
+
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index 415cc663f4..214c134441 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -4337,6 +4337,82 @@
         ]
       }
     },
+    "/search/album": {
+      "get": {
+        "operationId": "searchAlbum",
+        "parameters": [
+          {
+            "name": "name",
+            "required": true,
+            "in": "query",
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "page",
+            "required": false,
+            "in": "query",
+            "description": "Page number for pagination",
+            "schema": {
+              "minimum": 1,
+              "default": 1,
+              "type": "number"
+            }
+          },
+          {
+            "name": "shared",
+            "required": false,
+            "in": "query",
+            "description": "true: only shared albums\nfalse: only non-shared own albums\nundefined: shared and owned albums",
+            "schema": {
+              "type": "boolean"
+            }
+          },
+          {
+            "name": "size",
+            "required": false,
+            "in": "query",
+            "description": "Number of items per page",
+            "schema": {
+              "minimum": 1,
+              "maximum": 1000,
+              "default": 500,
+              "type": "number"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/SearchAlbumNameResponseDto"
+                }
+              }
+            },
+            "description": ""
+          }
+        },
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "tags": [
+          "Search"
+        ],
+        "x-immich-lifecycle": {
+          "addedAt": "v1.118.0"
+        }
+      }
+    },
     "/search/cities": {
       "get": {
         "operationId": "getAssetsByCity",
@@ -4461,6 +4537,29 @@
               "type": "string"
             }
           },
+          {
+            "name": "page",
+            "required": false,
+            "in": "query",
+            "description": "This property was added in v118.0.0",
+            "schema": {
+              "minimum": 1,
+              "default": 1,
+              "type": "number"
+            }
+          },
+          {
+            "name": "size",
+            "required": false,
+            "in": "query",
+            "description": "This property was added in v118.0.0",
+            "schema": {
+              "minimum": 1,
+              "maximum": 1000,
+              "default": 500,
+              "type": "number"
+            }
+          },
           {
             "name": "withHidden",
             "required": false,
@@ -4475,10 +4574,7 @@
             "content": {
               "application/json": {
                 "schema": {
-                  "items": {
-                    "$ref": "#/components/schemas/PersonResponseDto"
-                  },
-                  "type": "array"
+                  "$ref": "#/components/schemas/SearchPersonNameResponseDto"
                 }
               }
             },
@@ -10552,6 +10648,26 @@
         ],
         "type": "object"
       },
+      "SearchAlbumNameResponseDto": {
+        "properties": {
+          "albums": {
+            "items": {
+              "$ref": "#/components/schemas/AlbumResponseDto"
+            },
+            "type": "array"
+          },
+          "hasNextPage": {
+            "type": "boolean"
+          },
+          "total": {
+            "type": "integer"
+          }
+        },
+        "required": [
+          "albums"
+        ],
+        "type": "object"
+      },
       "SearchAlbumResponseDto": {
         "properties": {
           "count": {
@@ -10681,6 +10797,26 @@
         ],
         "type": "object"
       },
+      "SearchPersonNameResponseDto": {
+        "properties": {
+          "hasNextPage": {
+            "type": "boolean"
+          },
+          "people": {
+            "items": {
+              "$ref": "#/components/schemas/PersonResponseDto"
+            },
+            "type": "array"
+          },
+          "total": {
+            "type": "integer"
+          }
+        },
+        "required": [
+          "people"
+        ],
+        "type": "object"
+      },
       "SearchResponseDto": {
         "properties": {
           "albums": {
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index 2077943bf8..ee53a7135f 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -752,6 +752,11 @@ export type FileChecksumResponseDto = {
 export type FileReportFixDto = {
     items: FileReportItemDto[];
 };
+export type SearchAlbumNameResponseDto = {
+    albums: AlbumResponseDto[];
+    hasNextPage?: boolean;
+    total?: number;
+};
 export type SearchExploreItem = {
     data: AssetResponseDto;
     value: string;
@@ -828,6 +833,11 @@ export type SearchResponseDto = {
     albums: SearchAlbumResponseDto;
     assets: SearchAssetResponseDto;
 };
+export type SearchPersonNameResponseDto = {
+    hasNextPage?: boolean;
+    people: PersonResponseDto[];
+    total?: number;
+};
 export type PlacesResponseDto = {
     admin1name?: string;
     admin2name?: string;
@@ -2459,6 +2469,24 @@ export function fixAuditFiles({ fileReportFixDto }: {
         body: fileReportFixDto
     })));
 }
+export function searchAlbum({ name, page, shared, size }: {
+    name: string;
+    page?: number;
+    shared?: boolean;
+    size?: number;
+}, opts?: Oazapfts.RequestOpts) {
+    return oazapfts.ok(oazapfts.fetchJson<{
+        status: 200;
+        data: SearchAlbumNameResponseDto;
+    }>(`/search/album${QS.query(QS.explode({
+        name,
+        page,
+        shared,
+        size
+    }))}`, {
+        ...opts
+    }));
+}
 export function getAssetsByCity(opts?: Oazapfts.RequestOpts) {
     return oazapfts.ok(oazapfts.fetchJson<{
         status: 200;
@@ -2487,15 +2515,19 @@ export function searchMetadata({ metadataSearchDto }: {
         body: metadataSearchDto
     })));
 }
-export function searchPerson({ name, withHidden }: {
+export function searchPerson({ name, page, size, withHidden }: {
     name: string;
+    page?: number;
+    size?: number;
     withHidden?: boolean;
 }, opts?: Oazapfts.RequestOpts) {
     return oazapfts.ok(oazapfts.fetchJson<{
         status: 200;
-        data: PersonResponseDto[];
+        data: SearchPersonNameResponseDto;
     }>(`/search/person${QS.query(QS.explode({
         name,
+        page,
+        size,
         withHidden
     }))}`, {
         ...opts
diff --git a/server/src/controllers/search.controller.ts b/server/src/controllers/search.controller.ts
index 9fdb2746fc..2d0ef26b18 100644
--- a/server/src/controllers/search.controller.ts
+++ b/server/src/controllers/search.controller.ts
@@ -1,14 +1,17 @@
 import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
+import { EndpointLifecycle } from 'src/decorators';
 import { AssetResponseDto } from 'src/dtos/asset-response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
-import { PersonResponseDto } from 'src/dtos/person.dto';
 import {
   MetadataSearchDto,
   PlacesResponseDto,
   RandomSearchDto,
+  SearchAlbumNameResponseDto,
+  SearchAlbumsDto,
   SearchExploreResponseDto,
   SearchPeopleDto,
+  SearchPersonNameResponseDto,
   SearchPlacesDto,
   SearchResponseDto,
   SearchSuggestionRequestDto,
@@ -51,7 +54,7 @@ export class SearchController {
 
   @Get('person')
   @Authenticated()
-  searchPerson(@Auth() auth: AuthDto, @Query() dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
+  searchPerson(@Auth() auth: AuthDto, @Query() dto: SearchPeopleDto): Promise<SearchPersonNameResponseDto> {
     return this.service.searchPerson(auth, dto);
   }
 
@@ -73,4 +76,11 @@ export class SearchController {
     // TODO fix open api generation to indicate that results can be nullable
     return this.service.getSearchSuggestions(auth, dto) as Promise<string[]>;
   }
+
+  @Get('album')
+  @EndpointLifecycle({ addedAt: 'v1.118.0' })
+  @Authenticated()
+  searchAlbum(@Auth() auth: AuthDto, @Query() dto: SearchAlbumsDto): Promise<SearchAlbumNameResponseDto> {
+    return this.service.searchAlbum(auth, dto);
+  }
 }
diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts
index b12847ee62..20a6969a45 100644
--- a/server/src/dtos/album.dto.ts
+++ b/server/src/dtos/album.dto.ts
@@ -192,5 +192,5 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt
   };
 };
 
-export const mapAlbumWithAssets = (entity: AlbumEntity) => mapAlbum(entity, true);
-export const mapAlbumWithoutAssets = (entity: AlbumEntity) => mapAlbum(entity, false);
+export const mapAlbumWithAssets = (entity: AlbumEntity): AlbumResponseDto => mapAlbum(entity, true);
+export const mapAlbumWithoutAssets = (entity: AlbumEntity): AlbumResponseDto => mapAlbum(entity, false);
diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts
index 5c5dce1a11..b0e823391d 100644
--- a/server/src/dtos/search.dto.ts
+++ b/server/src/dtos/search.dto.ts
@@ -1,9 +1,10 @@
-import { ApiProperty } from '@nestjs/swagger';
+import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
 import { Type } from 'class-transformer';
 import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
 import { PropertyLifecycle } from 'src/decorators';
 import { AlbumResponseDto } from 'src/dtos/album.dto';
 import { AssetResponseDto } from 'src/dtos/asset-response.dto';
+import { PersonResponseDto } from 'src/dtos/person.dto';
 import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
 import { AssetOrder, AssetType } from 'src/enum';
 import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
@@ -184,12 +185,53 @@ export class SmartSearchDto extends BaseSearchDto {
   page?: number;
 }
 
-export class SearchPlacesDto {
+export class SearchDto {
   @IsString()
   @IsNotEmpty()
   name!: string;
 }
 
+export class SearchPlacesDto extends SearchDto {}
+
+export class SearchAlbumsDto extends SearchDto {
+  /**
+   * true: only shared albums
+   * false: only non-shared own albums
+   * undefined: shared and owned albums
+   */
+  @ValidateBoolean({ optional: true })
+  shared?: boolean;
+
+  /** Page number for pagination */
+  @ApiPropertyOptional()
+  @IsInt()
+  @Min(1)
+  @Type(() => Number)
+  page: number = 1;
+
+  /** Number of items per page */
+  @ApiPropertyOptional()
+  @IsInt()
+  @Min(1)
+  @Max(1000)
+  @Type(() => Number)
+  size: number = 500;
+}
+
+export class SearchPersonNameResponseDto {
+  people!: PersonResponseDto[];
+  @ApiProperty({ type: 'integer' })
+  total?: number;
+  hasNextPage?: boolean;
+}
+
+export class SearchAlbumNameResponseDto {
+  albums!: AlbumResponseDto[];
+  @ApiProperty({ type: 'integer' })
+  total?: number;
+  hasNextPage?: boolean;
+}
+
 export class SearchPeopleDto {
   @IsString()
   @IsNotEmpty()
@@ -197,6 +239,23 @@ export class SearchPeopleDto {
 
   @ValidateBoolean({ optional: true })
   withHidden?: boolean;
+
+  /** Page number for pagination */
+  @ApiPropertyOptional()
+  @PropertyLifecycle({ addedAt: 'v118.0.0' })
+  @IsInt()
+  @Min(1)
+  @Type(() => Number)
+  page: number = 1;
+
+  /** Number of items per page */
+  @ApiPropertyOptional()
+  @PropertyLifecycle({ addedAt: 'v118.0.0' })
+  @IsInt()
+  @Min(1)
+  @Max(1000)
+  @Type(() => Number)
+  size: number = 500;
 }
 
 export class PlacesResponseDto {
diff --git a/server/src/interfaces/album.interface.ts b/server/src/interfaces/album.interface.ts
index 24c64bdc9d..01acdfc7aa 100644
--- a/server/src/interfaces/album.interface.ts
+++ b/server/src/interfaces/album.interface.ts
@@ -1,5 +1,6 @@
 import { AlbumEntity } from 'src/entities/album.entity';
 import { IBulkAsset } from 'src/utils/asset.util';
+import { Paginated, PaginationOptions } from 'src/utils/pagination';
 
 export const IAlbumRepository = 'IAlbumRepository';
 
@@ -29,4 +30,5 @@ export interface IAlbumRepository extends IBulkAsset {
   update(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
   delete(id: string): Promise<void>;
   updateThumbnails(): Promise<number | undefined>;
+  getByName(pagination: PaginationOptions, userId: string, albumName: string, shared?: boolean): Paginated<AlbumEntity>;
 }
diff --git a/server/src/interfaces/person.interface.ts b/server/src/interfaces/person.interface.ts
index b3e2c0990e..c353fc5fcc 100644
--- a/server/src/interfaces/person.interface.ts
+++ b/server/src/interfaces/person.interface.ts
@@ -52,7 +52,12 @@ export interface IPersonRepository {
   getAllForUser(pagination: PaginationOptions, userId: string, options: PersonSearchOptions): Paginated<PersonEntity>;
   getAllWithoutFaces(): Promise<PersonEntity[]>;
   getById(personId: string): Promise<PersonEntity | null>;
-  getByName(userId: string, personName: string, options: PersonNameSearchOptions): Promise<PersonEntity[]>;
+  getByName(
+    pagination: PaginationOptions,
+    userId: string,
+    personName: string,
+    options: PersonNameSearchOptions,
+  ): Paginated<PersonEntity>;
   getDistinctNames(userId: string, options: PersonNameSearchOptions): Promise<PersonNameResponse[]>;
 
   create(person: Partial<PersonEntity>): Promise<PersonEntity>;
diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql
index c4f6fbdd32..e9372a18c1 100644
--- a/server/src/queries/album.repository.sql
+++ b/server/src/queries/album.repository.sql
@@ -520,3 +520,71 @@ WHERE
       "album_assets"."albumsId" = "albums"."id"
       AND "albums"."albumThumbnailAssetId" = "album_assets"."assetsId"
   )
+
+-- AlbumRepository.getByName
+SELECT
+  COUNT(DISTINCT ("album"."id")) AS "cnt"
+FROM
+  "albums" "album"
+  LEFT JOIN "users" "owner" ON "owner"."id" = "album"."ownerId"
+  AND ("owner"."deletedAt" IS NULL)
+  LEFT JOIN "albums_shared_users_users" "album_users" ON "album_users"."albumsId" = "album"."id"
+WHERE
+  (
+    (
+      "album"."ownerId" = $1
+      OR "album_users"."usersId" = $1
+    )
+    AND (
+      LOWER("album"."albumName") LIKE $2
+      OR LOWER("album"."albumName") LIKE $3
+    )
+  )
+  AND ("album"."deletedAt" IS NULL)
+SELECT
+  "album"."id" AS "album_id",
+  "album"."ownerId" AS "album_ownerId",
+  "album"."albumName" AS "album_albumName",
+  "album"."description" AS "album_description",
+  "album"."createdAt" AS "album_createdAt",
+  "album"."updatedAt" AS "album_updatedAt",
+  "album"."deletedAt" AS "album_deletedAt",
+  "album"."albumThumbnailAssetId" AS "album_albumThumbnailAssetId",
+  "album"."isActivityEnabled" AS "album_isActivityEnabled",
+  "album"."order" AS "album_order",
+  "owner"."id" AS "owner_id",
+  "owner"."name" AS "owner_name",
+  "owner"."isAdmin" AS "owner_isAdmin",
+  "owner"."email" AS "owner_email",
+  "owner"."storageLabel" AS "owner_storageLabel",
+  "owner"."oauthId" AS "owner_oauthId",
+  "owner"."profileImagePath" AS "owner_profileImagePath",
+  "owner"."shouldChangePassword" AS "owner_shouldChangePassword",
+  "owner"."createdAt" AS "owner_createdAt",
+  "owner"."deletedAt" AS "owner_deletedAt",
+  "owner"."status" AS "owner_status",
+  "owner"."updatedAt" AS "owner_updatedAt",
+  "owner"."quotaSizeInBytes" AS "owner_quotaSizeInBytes",
+  "owner"."quotaUsageInBytes" AS "owner_quotaUsageInBytes",
+  "owner"."profileChangedAt" AS "owner_profileChangedAt"
+FROM
+  "albums" "album"
+  LEFT JOIN "users" "owner" ON "owner"."id" = "album"."ownerId"
+  AND ("owner"."deletedAt" IS NULL)
+  LEFT JOIN "albums_shared_users_users" "album_users" ON "album_users"."albumsId" = "album"."id"
+WHERE
+  (
+    (
+      "album"."ownerId" = $1
+      OR "album_users"."usersId" = $1
+    )
+    AND (
+      LOWER("album"."albumName") LIKE $2
+      OR LOWER("album"."albumName") LIKE $3
+    )
+  )
+  AND ("album"."deletedAt" IS NULL)
+LIMIT
+  11
+OFFSET
+  10
diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql
index 5616559d7d..76e11f7d05 100644
--- a/server/src/queries/person.repository.sql
+++ b/server/src/queries/person.repository.sql
@@ -204,6 +204,16 @@ WHERE
   "id" = $2
 
 -- PersonRepository.getByName
+SELECT
+  COUNT(1) AS "cnt"
+FROM
+  "person" "person"
+WHERE
+  "person"."ownerId" = $1
+  AND (
+    LOWER("person"."name") LIKE $2
+    OR LOWER("person"."name") LIKE $3
+  )
 SELECT
   "person"."id" AS "person_id",
   "person"."createdAt" AS "person_createdAt",
@@ -223,7 +233,9 @@ WHERE
     OR LOWER("person"."name") LIKE $3
   )
 LIMIT
-  1000
+  11
+OFFSET
+  10
 
 -- PersonRepository.getDistinctNames
 SELECT DISTINCT
diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts
index f7b4cb44aa..cabd64d034 100644
--- a/server/src/repositories/album.repository.ts
+++ b/server/src/repositories/album.repository.ts
@@ -3,8 +3,10 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
 import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
 import { AlbumEntity } from 'src/entities/album.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
+import { PaginationMode } from 'src/enum';
 import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
 import { Instrumentation } from 'src/utils/instrumentation';
+import { Paginated, paginatedBuilder, PaginationOptions } from 'src/utils/pagination';
 import {
   DataSource,
   EntityManager,
@@ -302,4 +304,54 @@ export class AlbumRepository implements IAlbumRepository {
 
     return result.affected;
   }
+
+  @GenerateSql({ params: [{ take: 10, skip: 10, withCount: true }, DummyValue.UUID, DummyValue.STRING, undefined] })
+  getByName(
+    pagination: PaginationOptions,
+    userId: string,
+    albumName: string,
+    shared?: boolean,
+  ): Paginated<AlbumEntity> {
+    const getAlbumSharedOptions = () => {
+      switch (shared) {
+        case true: {
+          return { owner: '(album_users.usersId = :userId)', options: '' };
+        }
+        case false: {
+          return {
+            owner: '(album.ownerId = :userId)',
+            options: 'AND album_users.usersId IS NULL AND shared_links.id IS NULL',
+          };
+        }
+        case undefined: {
+          return { owner: '(album.ownerId = :userId OR album_users.usersId = :userId)', options: '' };
+        }
+      }
+    };
+
+    const albumSharedOptions = getAlbumSharedOptions();
+
+    let queryBuilder = this.repository
+      .createQueryBuilder('album')
+      .leftJoinAndSelect('album.owner', 'owner')
+      .leftJoin('albums_shared_users_users', 'album_users', 'album_users.albumsId = album.id');
+
+    if (shared === false) {
+      queryBuilder = queryBuilder.leftJoin('shared_links', 'shared_links', 'shared_links.albumId = album.id');
+    }
+
+    queryBuilder = queryBuilder.where(
+      `${albumSharedOptions.owner} AND (LOWER(album.albumName) LIKE :nameStart OR LOWER(album.albumName) LIKE :nameAnywhere) ${albumSharedOptions.options}`,
+      {
+        userId,
+        nameStart: `${albumName.toLowerCase()}%`,
+        nameAnywhere: `% ${albumName.toLowerCase()}%`,
+      },
+    );
+
+    return paginatedBuilder(queryBuilder, {
+      mode: PaginationMode.LIMIT_OFFSET,
+      ...pagination,
+    });
+  }
 }
diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts
index c62c4b8739..5c33e7c09d 100644
--- a/server/src/repositories/person.repository.ts
+++ b/server/src/repositories/person.repository.ts
@@ -183,8 +183,15 @@ export class PersonRepository implements IPersonRepository {
     return this.personRepository.findOne({ where: { id: personId } });
   }
 
-  @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, { withHidden: true }] })
-  getByName(userId: string, personName: string, { withHidden }: PersonNameSearchOptions): Promise<PersonEntity[]> {
+  @GenerateSql({
+    params: [{ take: 10, skip: 10, withCount: true }, DummyValue.UUID, DummyValue.STRING, { withHidden: true }],
+  })
+  getByName(
+    pagination: PaginationOptions,
+    userId: string,
+    personName: string,
+    { withHidden }: PersonNameSearchOptions,
+  ): Paginated<PersonEntity> {
     const queryBuilder = this.personRepository
       .createQueryBuilder('person')
       .where(
@@ -196,7 +203,10 @@ export class PersonRepository implements IPersonRepository {
     if (!withHidden) {
       queryBuilder.andWhere('person.isHidden = false');
     }
-    return queryBuilder.getMany();
+    return paginatedBuilder(queryBuilder, {
+      mode: PaginationMode.LIMIT_OFFSET,
+      ...pagination,
+    });
   }
 
   @GenerateSql({ params: [DummyValue.UUID, { withHidden: true }] })
diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts
index e0b03f31ae..4526ed0183 100644
--- a/server/src/services/search.service.spec.ts
+++ b/server/src/services/search.service.spec.ts
@@ -1,9 +1,11 @@
 import { mapAsset } from 'src/dtos/asset-response.dto';
 import { SearchSuggestionType } from 'src/dtos/search.dto';
+import { IAlbumRepository } from 'src/interfaces/album.interface';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
 import { IPersonRepository } from 'src/interfaces/person.interface';
 import { ISearchRepository } from 'src/interfaces/search.interface';
 import { SearchService } from 'src/services/search.service';
+import { albumStub } from 'test/fixtures/album.stub';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { authStub } from 'test/fixtures/auth.stub';
 import { personStub } from 'test/fixtures/person.stub';
@@ -15,12 +17,13 @@ vitest.useFakeTimers();
 describe(SearchService.name, () => {
   let sut: SearchService;
 
+  let albumMock: Mocked<IAlbumRepository>;
   let assetMock: Mocked<IAssetRepository>;
   let personMock: Mocked<IPersonRepository>;
   let searchMock: Mocked<ISearchRepository>;
 
   beforeEach(() => {
-    ({ sut, assetMock, personMock, searchMock } = newTestService(SearchService));
+    ({ sut, albumMock, assetMock, personMock, searchMock } = newTestService(SearchService));
   });
 
   it('should work', () => {
@@ -29,15 +32,91 @@ describe(SearchService.name, () => {
 
   describe('searchPerson', () => {
     it('should pass options to search', async () => {
+      personMock.getByName.mockResolvedValue({
+        items: [personStub.withName],
+        hasNextPage: false,
+      });
       const { name } = personStub.withName;
 
-      await sut.searchPerson(authStub.user1, { name, withHidden: false });
+      await sut.searchPerson(authStub.user1, {
+        name,
+        withHidden: false,
+        page: 1,
+        size: 500,
+      });
 
-      expect(personMock.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: false });
+      expect(personMock.getByName).toHaveBeenCalledWith(
+        {
+          skip: 0,
+          take: 500,
+          withCount: true,
+        },
+        authStub.user1.user.id,
+        name,
+        { withHidden: false },
+      );
 
-      await sut.searchPerson(authStub.user1, { name, withHidden: true });
+      await sut.searchPerson(authStub.user1, { name, withHidden: true, page: 1, size: 500 });
 
-      expect(personMock.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: true });
+      expect(personMock.getByName).toHaveBeenCalledWith(
+        {
+          skip: 0,
+          take: 500,
+          withCount: true,
+        },
+        authStub.user1.user.id,
+        name,
+        { withHidden: true },
+      );
+    });
+  });
+
+  describe('searchAlbum', () => {
+    it('should pass options to search', async () => {
+      albumMock.getByName.mockResolvedValue({
+        items: [albumStub.twoAssets],
+        hasNextPage: false,
+      });
+      albumMock.getMetadataForIds.mockResolvedValue([
+        { albumId: albumStub.twoAssets.id, assetCount: 2, startDate: undefined, endDate: undefined },
+      ]);
+      const { albumName } = albumStub.twoAssets;
+
+      await sut.searchAlbum(authStub.user1, {
+        name: albumName,
+        shared: true,
+        page: 1,
+        size: 500,
+      });
+
+      expect(albumMock.getByName).toHaveBeenCalledWith(
+        {
+          skip: 0,
+          take: 500,
+          withCount: true,
+        },
+        authStub.user1.user.id,
+        albumName,
+        true,
+      );
+
+      await sut.searchAlbum(authStub.user1, {
+        name: albumName,
+        shared: false,
+        page: 1,
+        size: 500,
+      });
+
+      expect(albumMock.getByName).toHaveBeenCalledWith(
+        {
+          skip: 0,
+          take: 500,
+          withCount: true,
+        },
+        authStub.user1.user.id,
+        albumName,
+        false,
+      );
     });
   });
 
diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts
index 03ffbe97db..06d8ddf501 100644
--- a/server/src/services/search.service.ts
+++ b/server/src/services/search.service.ts
@@ -1,21 +1,27 @@
 import { BadRequestException, Injectable } from '@nestjs/common';
+import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto';
+
 import { AssetMapOptions, AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
-import { PersonResponseDto } from 'src/dtos/person.dto';
+import { mapPerson } from 'src/dtos/person.dto';
 import {
+  mapPlaces,
   MetadataSearchDto,
   PlacesResponseDto,
   RandomSearchDto,
+  SearchAlbumNameResponseDto,
+  SearchAlbumsDto,
   SearchPeopleDto,
+  SearchPersonNameResponseDto,
   SearchPlacesDto,
   SearchResponseDto,
   SearchSuggestionRequestDto,
   SearchSuggestionType,
   SmartSearchDto,
-  mapPlaces,
 } from 'src/dtos/search.dto';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { AssetOrder } from 'src/enum';
+import { AlbumAssetCount } from 'src/interfaces/album.interface';
 import { SearchExploreItem } from 'src/interfaces/search.interface';
 import { BaseService } from 'src/services/base.service';
 import { getMyPartnerIds } from 'src/utils/asset.util';
@@ -23,8 +29,65 @@ import { isSmartSearchEnabled } from 'src/utils/misc';
 
 @Injectable()
 export class SearchService extends BaseService {
-  async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
-    return this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden });
+  async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<SearchPersonNameResponseDto> {
+    const { withHidden, name, page, size } = dto;
+    const pagination = {
+      take: size,
+      skip: (page - 1) * size,
+      withCount: true,
+    };
+    const { items, hasNextPage, count } = await this.personRepository.getByName(pagination, auth.user.id, name, {
+      withHidden,
+    });
+
+    const people = items.map((person) => mapPerson(person));
+
+    return {
+      people,
+      hasNextPage,
+      total: count,
+    };
+  }
+
+  async searchAlbum(auth: AuthDto, dto: SearchAlbumsDto): Promise<SearchAlbumNameResponseDto> {
+    const { shared, name, page, size } = dto;
+    const pagination = {
+      take: size,
+      skip: (page - 1) * size,
+      withCount: true,
+    };
+    const { items, hasNextPage, count } = await this.albumRepository.getByName(pagination, auth.user.id, name, shared);
+    const results = await this.albumRepository.getMetadataForIds(items.map((album) => album.id));
+    const albumMetadata: Record<string, AlbumAssetCount> = {};
+    for (const metadata of results) {
+      const { albumId, assetCount, startDate, endDate } = metadata;
+      albumMetadata[albumId] = {
+        albumId,
+        assetCount,
+        startDate,
+        endDate,
+      };
+    }
+
+    const albums: AlbumResponseDto[] = await Promise.all(
+      items.map(async (album) => {
+        const lastModifiedAsset = await this.assetRepository.getLastUpdatedAssetForAlbumId(album.id);
+        return {
+          ...mapAlbumWithoutAssets(album),
+          sharedLinks: undefined,
+          startDate: albumMetadata[album.id].startDate,
+          endDate: albumMetadata[album.id].endDate,
+          assetCount: albumMetadata[album.id].assetCount,
+          lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt,
+        };
+      }),
+    );
+
+    return {
+      albums,
+      total: count,
+      hasNextPage,
+    };
   }
 
   async searchPlaces(dto: SearchPlacesDto): Promise<PlacesResponseDto[]> {
diff --git a/server/src/utils/pagination.ts b/server/src/utils/pagination.ts
index 4009f219c1..208ac2a222 100644
--- a/server/src/utils/pagination.ts
+++ b/server/src/utils/pagination.ts
@@ -5,17 +5,20 @@ import { FindManyOptions, ObjectLiteral, Repository, SelectQueryBuilder } from '
 export interface PaginationOptions {
   take: number;
   skip?: number;
+  withCount?: boolean;
 }
 
 export interface PaginatedBuilderOptions {
   take: number;
   skip?: number;
   mode?: PaginationMode;
+  withCount?: boolean;
 }
 
 export interface PaginationResult<T> {
   items: T[];
   hasNextPage: boolean;
+  count?: number;
 }
 
 export type Paginated<T> = Promise<PaginationResult<T>>;
@@ -33,11 +36,15 @@ export async function* usePagination<T>(
   }
 }
 
-function paginationHelper<Entity extends ObjectLiteral>(items: Entity[], take: number): PaginationResult<Entity> {
+function paginationHelper<Entity extends ObjectLiteral>(
+  items: Entity[],
+  take: number,
+  count?: number,
+): PaginationResult<Entity> {
   const hasNextPage = items.length > take;
   items.splice(take);
 
-  return { items, hasNextPage };
+  return { items, hasNextPage, count };
 }
 
 export async function paginate<Entity extends ObjectLiteral>(
@@ -62,7 +69,7 @@ export async function paginate<Entity extends ObjectLiteral>(
 
 export async function paginatedBuilder<Entity extends ObjectLiteral>(
   qb: SelectQueryBuilder<Entity>,
-  { take, skip, mode }: PaginatedBuilderOptions,
+  { take, skip, mode, withCount }: PaginatedBuilderOptions,
 ): Paginated<Entity> {
   if (mode === PaginationMode.LIMIT_OFFSET) {
     qb.limit(take + 1).offset(skip);
@@ -70,6 +77,8 @@ export async function paginatedBuilder<Entity extends ObjectLiteral>(
     qb.take(take + 1).skip(skip);
   }
 
+  const count = withCount ? await qb.getCount() : undefined;
+
   const items = await qb.getMany();
-  return paginationHelper(items, take);
+  return paginationHelper(items, take, count);
 }
diff --git a/server/test/repositories/album.repository.mock.ts b/server/test/repositories/album.repository.mock.ts
index dd5c3af6a8..97f86576d5 100644
--- a/server/test/repositories/album.repository.mock.ts
+++ b/server/test/repositories/album.repository.mock.ts
@@ -20,5 +20,6 @@ export const newAlbumRepositoryMock = (): Mocked<IAlbumRepository> => {
     update: vitest.fn(),
     delete: vitest.fn(),
     updateThumbnails: vitest.fn(),
+    getByName: vitest.fn(),
   };
 };
diff --git a/web/src/lib/components/faces-page/people-search.svelte b/web/src/lib/components/faces-page/people-search.svelte
index 2a952b8145..221fb48be6 100644
--- a/web/src/lib/components/faces-page/people-search.svelte
+++ b/web/src/lib/components/faces-page/people-search.svelte
@@ -22,6 +22,8 @@
   let abortController: AbortController | null = null;
   let timeout: NodeJS.Timeout | null = null;
 
+  // TODO: use pagination
+
   const search = () => {
     searchedPeopleLocal = searchNameLocal(searchName, searchedPeople, numberPeopleToSearch);
   };
@@ -58,8 +60,11 @@
     abortController = new AbortController();
     timeout = setTimeout(() => (showLoadingSpinner = true), timeBeforeShowLoadingSpinner);
     try {
-      const data = await searchPerson({ name: searchName }, { signal: abortController?.signal });
-      searchedPeople = data;
+      const data = await searchPerson(
+        { name: searchName, size: maximumLengthSearchPeople },
+        { signal: abortController?.signal },
+      );
+      searchedPeople = data.people;
       searchWord = searchName;
     } catch (error) {
       handleError(error, $t('errors.cant_search_people'));
diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte
index b6d25c48bf..fc5e4316e8 100644
--- a/web/src/routes/(user)/people/+page.svelte
+++ b/web/src/routes/(user)/people/+page.svelte
@@ -208,11 +208,11 @@
       await changeName();
       return;
     }
-    const data = await searchPerson({ name: personName, withHidden: true });
+    const data = await searchPerson({ name: personName, size: 5, withHidden: true });
 
     // We check if another person has the same name as the name entered by the user
 
-    const existingPerson = data.find(
+    const existingPerson = data.people.find(
       (person: PersonResponseDto) =>
         person.name.toLowerCase() === personName.toLowerCase() &&
         edittingPerson &&
diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 037feaf35f..b72cd9bdba 100644
--- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -284,15 +284,16 @@
       return;
     }
 
-    const result = await searchPerson({ name: personName, withHidden: true });
+    // We need to search for 5 people: we show only 3 people for the SUGGEST_MERGE view mode + the person to merge + the person to be merged in = 5
+    const result = await searchPerson({ name: personName, size: 5, withHidden: true });
 
-    const existingPerson = result.find(
+    const existingPerson = result.people.find(
       ({ name, id }: PersonResponseDto) => name.toLowerCase() === personName.toLowerCase() && id !== person.id && name,
     );
     if (existingPerson) {
       personMerge2 = existingPerson;
       personMerge1 = person;
-      potentialMergePeople = result
+      potentialMergePeople = result.people
         .filter(
           (person: PersonResponseDto) =>
             personMerge2.name.toLowerCase() === person.name.toLowerCase() &&