diff --git a/CRM/Campaign/Selector/Search.php b/CRM/Campaign/Selector/Search.php
index 2c3cdd4856a79d2371413ab05d7483952545ef87..097178b9bae93f2fcf02355ac3fa4264b41450b8 100644
--- a/CRM/Campaign/Selector/Search.php
+++ b/CRM/Campaign/Selector/Search.php
@@ -249,7 +249,7 @@ class CRM_Campaign_Selector_Search extends CRM_Core_Selector_Base implements CRM
   /**
    * @param $sort
    */
-  public function buildPrevNextCache($sort) {
+  private function buildPrevNextCache($sort) {
     //for prev/next pagination
     $crmPID = CRM_Utils_Request::retrieve('crmPID', 'Integer');
 
diff --git a/CRM/Contact/Selector.php b/CRM/Contact/Selector.php
index 54282ce74bb086029a596691b02826a9da95f5ac..fa8d2da1f6cb571f286658b018132c86cc100b3c 100644
--- a/CRM/Contact/Selector.php
+++ b/CRM/Contact/Selector.php
@@ -864,7 +864,7 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
    *
    * @return string
    */
-  public function buildPrevNextCache($sort) {
+  private function buildPrevNextCache($sort) {
     $cacheKey = 'civicrm search ' . $this->_key;
 
     // We should clear the cache in following conditions:
@@ -1033,18 +1033,9 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
    *
    * @throws \CRM_Core_Exception
    */
-  public function fillupPrevNextCache($sort, $cacheKey, $start = 0, $end = self::CACHE_SIZE) {
-    $coreSearch = TRUE;
-    // For custom searches, use the contactIDs method
-    if (is_a($this, 'CRM_Contact_Selector_Custom')) {
-      $sql = $this->_search->contactIDs($start, $end, $sort, TRUE);
-      $coreSearch = FALSE;
-    }
-    // For core searches use the searchQuery method
-    else {
-      $sql = $this->_query->getSearchSQL($start, $end, $sort, FALSE, $this->_query->_includeContactIds,
+  private function fillupPrevNextCache($sort, $cacheKey, $start = 0, $end = self::CACHE_SIZE) {
+    $sql = $this->_query->getSearchSQL($start, $end, $sort, FALSE, $this->_query->_includeContactIds,
         FALSE, TRUE);
-    }
 
     // CRM-9096
     // due to limitations in our search query writer, the above query does not work
@@ -1055,7 +1046,7 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
     // the other alternative of running the FULL query will just be incredibly inefficient
     // and slow things down way too much on large data sets / complex queries
 
-    $selectSQL = CRM_Core_DAO::composeQuery("SELECT DISTINCT %1, contact_a.id, contact_a.sort_name", [1 => [$cacheKey, 'String']]);
+    $selectSQL = CRM_Core_DAO::composeQuery('SELECT DISTINCT %1, contact_a.id, contact_a.sort_name', [1 => [$cacheKey, 'String']]);
 
     $sql = str_ireplace(['SELECT contact_a.id as contact_id', 'SELECT contact_a.id as id'], $selectSQL, $sql);
     $sql = str_ireplace('ORDER BY `contact_id`', 'ORDER BY `id`', $sql, $sql);
@@ -1064,19 +1055,9 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
       Civi::service('prevnext')->fillWithSql($cacheKey, $sql);
     }
     catch (\Exception $e) {
-      if ($coreSearch) {
-        // in the case of error, try rebuilding cache using full sql which is used for search selector display
-        // this fixes the bugs reported in CRM-13996 & CRM-14438
-        $this->rebuildPreNextCache($start, $end, $sort, $cacheKey);
-      }
-      else {
-        CRM_Core_Error::deprecatedFunctionWarning('Custom searches should return sql capable of filling the prevnext cache.');
-        // This will always show for CiviRules :-( as a) it orders by 'rule_label'
-        // which is not available in the query & b) it uses contact not contact_a
-        // as an alias.
-        // CRM_Core_Session::setStatus(ts('Query Failed'));
-        return;
-      }
+      // in the case of error, try rebuilding cache using full sql which is used for search selector display
+      // this fixes the bugs reported in CRM-13996 & CRM-14438
+      $this->rebuildPreNextCache($start, $end, $sort, $cacheKey);
     }
 
     if (Civi::service('prevnext') instanceof CRM_Core_PrevNextCache_Sql) {
@@ -1097,7 +1078,7 @@ class CRM_Contact_Selector extends CRM_Core_Selector_Base implements CRM_Core_Se
    * @param string $cacheKey
    *   Cache key.
    */
-  public function rebuildPreNextCache($start, $end, $sort, $cacheKey) {
+  private function rebuildPreNextCache($start, $end, $sort, $cacheKey): void {
     // generate full SQL
     $sql = $this->_query->searchQuery($start, $end, $sort, FALSE, $this->_query->_includeContactIds,
       FALSE, FALSE, TRUE);
diff --git a/ext/legacycustomsearches/CRM/Contact/Selector/Custom.php b/ext/legacycustomsearches/CRM/Contact/Selector/Custom.php
index 95507319a9692364566376056a6e06c533914a87..7c0a3b50f39c09658701a0e0675a9ada830766ea 100644
--- a/ext/legacycustomsearches/CRM/Contact/Selector/Custom.php
+++ b/ext/legacycustomsearches/CRM/Contact/Selector/Custom.php
@@ -13,7 +13,6 @@
  *
  * @package CRM
  * @copyright CiviCRM LLC https://civicrm.org/licensing
- * $Id: Selector.php 11510 2007-09-18 09:21:34Z lobo $
  */
 
 /**
@@ -28,7 +27,7 @@ class CRM_Contact_Selector_Custom extends CRM_Contact_Selector {
    *
    * @var array
    */
-  public static $_links = NULL;
+  public static $_links;
 
   /**
    * We use desc to remind us what that column is, name is used in the tpl
@@ -360,6 +359,107 @@ class CRM_Contact_Selector_Custom extends CRM_Contact_Selector {
     return $rows;
   }
 
+  /**
+   * @param CRM_Utils_Sort $sort
+   *
+   * @return string
+   * @throws \CRM_Core_Exception
+   */
+  private function buildPrevNextCache($sort): string {
+    $cacheKey = 'civicrm search ' . $this->_key;
+
+    // We should clear the cache in following conditions:
+    // 1. when starting from scratch, i.e new search
+    // 2. if records are sorted
+
+    // get current page requested
+    $pageNum = CRM_Utils_Request::retrieve('crmPID', 'Integer');
+
+    // get the current sort order
+    $currentSortID = CRM_Utils_Request::retrieve('crmSID', 'String');
+
+    $session = CRM_Core_Session::singleton();
+
+    // get previous sort id
+    $previousSortID = $session->get('previousSortID');
+
+    // check for current != previous to ensure cache is not reset if paging is done without changing
+    // sort criteria
+    if (!$pageNum || (!empty($currentSortID) && $currentSortID != $previousSortID)) {
+      Civi::service('prevnext')->deleteItem(NULL, $cacheKey, 'civicrm_contact');
+      // this means it's fresh search, so set pageNum=1
+      if (!$pageNum) {
+        $pageNum = 1;
+      }
+    }
+
+    // set the current sort as previous sort
+    if (!empty($currentSortID)) {
+      $session->set('previousSortID', $currentSortID);
+    }
+
+    $pageSize = CRM_Utils_Request::retrieve('crmRowCount', 'Integer', CRM_Core_DAO::$_nullObject, FALSE, 50);
+    $firstRecord = ($pageNum - 1) * $pageSize;
+
+    //for alphabetic pagination selection save
+    $sortByCharacter = CRM_Utils_Request::retrieve('sortByCharacter', 'String');
+
+    //for text field pagination selection save
+    $countRow = Civi::service('prevnext')->getCount($cacheKey);
+    // $sortByCharacter triggers a refresh in the prevNext cache
+    if ($sortByCharacter && $sortByCharacter !== 'all') {
+      $this->fillPrevNextCache($sort, $cacheKey, 0, max(self::CACHE_SIZE, $pageSize));
+    }
+    elseif (($firstRecord + $pageSize) >= $countRow) {
+      $this->fillPrevNextCache($sort, $cacheKey, $countRow, max(self::CACHE_SIZE, $pageSize) + $firstRecord - $countRow);
+    }
+    return $cacheKey;
+  }
+
+  /**
+   * @param CRM_Utils_Sort $sort
+   * @param string $cacheKey
+   * @param int $start
+   * @param int $end
+   *
+   * @throws \CRM_Core_Exception
+   */
+  public function fillPrevNextCache($sort, $cacheKey, $start = 0, $end = self::CACHE_SIZE): void {
+    $sql = $this->_search->contactIDs($start, $end, $sort, TRUE);
+
+    // CRM-9096
+    // due to limitations in our search query writer, the above query does not work
+    // in cases where the query is being sorted on a non-contact table
+    // this results in a fatal error :(
+    // see below for the gross hack of trapping the error and not filling
+    // the prev next cache in this situation
+    // the other alternative of running the FULL query will just be incredibly inefficient
+    // and slow things down way too much on large data sets / complex queries
+
+    $selectSQL = CRM_Core_DAO::composeQuery("SELECT DISTINCT %1, contact_a.id, contact_a.sort_name", [1 => [$cacheKey, 'String']]);
+
+    $sql = str_ireplace(['SELECT contact_a.id as contact_id', 'SELECT contact_a.id as id'], $selectSQL, $sql);
+    $sql = str_ireplace('ORDER BY `contact_id`', 'ORDER BY `id`', $sql, $sql);
+
+    try {
+      Civi::service('prevnext')->fillWithSql($cacheKey, $sql);
+    }
+    catch (\Exception $e) {
+      CRM_Core_Error::deprecatedFunctionWarning('Custom searches should return sql capable of filling the prevnext cache.');
+      // This will always show for CiviRules :-( as a) it orders by 'rule_label'
+      // which is not available in the query & b) it uses contact not contact_a
+      // as an alias.
+      // CRM_Core_Session::setStatus(ts('Query Failed'));
+      return;
+    }
+
+    if (Civi::service('prevnext') instanceof CRM_Core_PrevNextCache_Sql) {
+      // SQL-backed prevnext cache uses an extra record for pruning the cache.
+      // Also ensure that caches stay alive for 2 days as per previous code
+      Civi::cache('prevNextCache')->set($cacheKey, $cacheKey, 60 * 60 * 24 * CRM_Core_PrevNextCache_Sql::cacheDays);
+    }
+  }
+
   /**
    * Given the current formValues, gets the query in local language.
    *