diff --git a/config/vufind/channels.ini b/config/vufind/channels.ini
index 522236fbaa3346b172cd16fd75f9241d0a643273..ec6ffc605fb19257b51218ef57688a22de1b3abb 100644
--- a/config/vufind/channels.ini
+++ b/config/vufind/channels.ini
@@ -13,6 +13,10 @@ default_home_source = "Solr"
 ;
 ; Available providers:
 ;
+; alphabrowse - Find nearby records in the alphabetical browse index; only
+; supported for Solr; requires you to generate the index as described at
+; https://vufind.org/wiki/indexing:alphabetical_heading_browse
+;
 ; facets - Find records matching facet values from search results (or try to find
 ; other records matching the facet values of a specific record).
 ;
@@ -32,9 +36,11 @@ home[] = "facets:provider.facets.home"
 ; Providers to use for record-based channels (order matters!)
 record[] = "similaritems"
 record[] = "facets"
+;record[] = "alphabrowse"
 ; Providers to use for search-based channels (order matters!)
 search[] = "facets"
 search[] = "similaritems"
+;search[] = "alphabrowse"
 
 ; This section controls which providers are used for Summon searches/records.
 ; See [source.Solr] above for more details.
@@ -47,6 +53,20 @@ record[] = "facets:provider.facets.Summon"
 ; Providers to use for search-based channels (order matters!)
 search[] = "facets:provider.facets.Summon"
 
+; This section contains default settings for the AlphaBrowse channel provider
+[provider.alphabrowse]
+; Which index to use (recommended: lcc or dewey for call number browsing)
+browseIndex = lcc
+; Which Solr field should we use to seed the index search? (recommended:
+; callnumber-raw for lcc, dewey-raw for dewey).
+solrField = callnumber-raw
+; How many rows before the current record to display
+rows_before = 10
+; Maximum number of records to examine for similar results.
+maxRecordsToExamine = 2
+; Number of results to include in each channel.
+channelSize = 20
+
 ; This section contains home-page-specific settings for the Facets channel provider
 [provider.facets.home]
 ; Facet fields to use (field name => description).
diff --git a/languages/en.ini b/languages/en.ini
index c7d9c55e5cdfa80e309a3fcfbe30faa83c44d3b1..5274d0bf6acc10774e25a8c983289abd0909cbe1 100644
--- a/languages/en.ini
+++ b/languages/en.ini
@@ -172,6 +172,7 @@ catalog_login_desc = "Enter your library catalog credentials."
 CD = "CD"
 Change Password = "Change Password"
 channel_add_more = "Add more channels like this"
+channel_browse = "Browse more records"
 channel_expand = "Explore related channels"
 channel_explore = "Explore Channels"
 channel_search = "Show items as search results"
@@ -627,6 +628,7 @@ My Holds = "Holds"
 My Profile = "Profile"
 Narrow Search = "Narrow Search"
 navigate_back = "Back"
+nearby_items = "Items Near "%%title%%""
 Need Help? = "Need Help?"
 New Item Feed = "New Item Feed"
 New Item Search = "New Item Search"
diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php
index e19a4297cfaee409ff4e218c968f846ae0f55a86..3391b5d612994655c09cca6ef9a14d9f7c26c2ef 100644
--- a/module/VuFind/config/module.config.php
+++ b/module/VuFind/config/module.config.php
@@ -339,6 +339,7 @@ $config = [
             ],
             'channelprovider' => [
                 'factories' => [
+                    'alphabrowse' => 'VuFind\ChannelProvider\Factory::getAlphaBrowse',
                     'facets' => 'VuFind\ChannelProvider\Factory::getFacets',
                     'listitems' => 'VuFind\ChannelProvider\Factory::getListItems',
                     'random' => 'VuFind\ChannelProvider\Factory::getRandom',
diff --git a/module/VuFind/src/VuFind/ChannelProvider/AlphaBrowse.php b/module/VuFind/src/VuFind/ChannelProvider/AlphaBrowse.php
new file mode 100644
index 0000000000000000000000000000000000000000..980aa4b145a1b563b870c0df9a0b7952ce6ec151
--- /dev/null
+++ b/module/VuFind/src/VuFind/ChannelProvider/AlphaBrowse.php
@@ -0,0 +1,316 @@
+<?php
+/**
+ * Alphabrowse channel provider.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2016.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Channels
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+namespace VuFind\ChannelProvider;
+use VuFind\RecordDriver\AbstractBase as RecordDriver;
+use VuFind\Record\Router as RecordRouter;
+use VuFind\Search\Base\Params, VuFind\Search\Base\Results;
+use VuFind\I18n\Translator\TranslatorAwareInterface;
+use VuFindSearch\Backend\Solr\Backend;
+use VuFindSearch\ParamBag;
+use Zend\Mvc\Controller\Plugin\Url;
+
+/**
+ * Alphabrowse channel provider.
+ *
+ * @category VuFind
+ * @package  Channels
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+class AlphaBrowse extends AbstractChannelProvider
+    implements TranslatorAwareInterface
+{
+    use \VuFind\I18n\Translator\TranslatorAwareTrait;
+
+    /**
+     * Number of results to include in each channel.
+     *
+     * @var int
+     */
+    protected $channelSize;
+
+    /**
+     * Maximum number of records to examine for similar results.
+     *
+     * @var int
+     */
+    protected $maxRecordsToExamine;
+
+    /**
+     * Search service
+     *
+     * @var \VuFindSearch\Service
+     */
+    protected $searchService;
+
+    /**
+     * Solr backend
+     *
+     * @var Backend
+     */
+    protected $solr;
+
+    /**
+     * URL helper
+     *
+     * @var Url
+     */
+    protected $url;
+
+    /**
+     * Record router
+     *
+     * @var RecordRouter
+     */
+    protected $recordRouter;
+
+    /**
+     * Browse index to search
+     *
+     * @var string
+     */
+    protected $browseIndex;
+
+    /**
+     * Solr field to use for search seed
+     *
+     * @var string
+     */
+    protected $solrField;
+
+    /**
+     * How many rows to show before the selected value
+     *
+     * @var int
+     */
+    protected $rowsBefore;
+
+    /**
+     * Constructor
+     *
+     * @param \VuFindSearch\Service $search  Search service
+     * @param Backend               $solr    Solr backend
+     * @param Url                   $url     URL helper
+     * @param RecordRouter          $router  Record router
+     * @param array                 $options Settings (optional)
+     */
+    public function __construct(\VuFindSearch\Service $search, Backend $solr,
+        Url $url, RecordRouter $router, array $options = []
+    ) {
+        $this->searchService = $search;
+        $this->solr = $solr;
+        $this->url = $url;
+        $this->recordRouter = $router;
+        $this->setOptions($options);
+    }
+
+    /**
+     * Set the options for the provider.
+     *
+     * @param array $options Options
+     *
+     * @return void
+     */
+    public function setOptions(array $options)
+    {
+        $this->channelSize = isset($options['channelSize'])
+            ? $options['channelSize'] : 20;
+        $this->maxRecordsToExamine = isset($options['maxRecordsToExamine'])
+            ? $options['maxRecordsToExamine'] : 2;
+        $this->browseIndex = isset($options['browseIndex']) ?
+            $options['browseIndex'] : 'lcc';
+        $this->solrField = isset($options['solrField']) ?
+            $options['solrField'] : 'callnumber-raw';
+        $this->rowsBefore = isset($options['rows_before']) ?
+            $options['rows_before'] : 10;
+    }
+
+    /**
+     * Return channel information derived from a record driver object.
+     *
+     * @param RecordDriver $driver       Record driver
+     * @param string       $channelToken Token identifying a single specific channel
+     * to load (if omitted, all channels will be loaded)
+     *
+     * @return array
+     */
+    public function getFromRecord(RecordDriver $driver, $channelToken = null)
+    {
+        // If we have a token and it doesn't match the record driver, we can't
+        // fetch any results!
+        if ($channelToken !== null && $channelToken !== $driver->getUniqueID()) {
+            return [];
+        }
+        $channel = $this->buildChannelFromRecord($driver);
+        return (count($channel['contents']) > 0) ? [$channel] : [];
+    }
+
+    /**
+     * Return channel information derived from a search results object.
+     *
+     * @param Results $results      Search results
+     * @param string  $channelToken Token identifying a single specific channel
+     * to load (if omitted, all channels will be loaded)
+     *
+     * @return array
+     */
+    public function getFromSearch(Results $results, $channelToken = null)
+    {
+        $channels = [];
+        foreach ($results->getResults() as $driver) {
+            // If we have a token and it doesn't match the current driver, skip
+            // that driver.
+            if ($channelToken !== null && $channelToken !== $driver->getUniqueID()) {
+                continue;
+            }
+            $channel = (count($channels) < $this->maxRecordsToExamine)
+                ? $this->buildChannelFromRecord($driver)
+                : $this->buildChannelFromRecord($driver, true);
+            if (isset($channel['token']) || count($channel['contents']) > 0) {
+                $channels[] = $channel;
+            }
+        }
+        // If the search results did not include the object we were looking for,
+        // we need to fetch it from the search service:
+        if (empty($channels) && is_object($driver) && $channelToken !== null) {
+            $driver = $this->searchService->retrieve(
+                $driver->getSourceIdentifier(), $channelToken
+            )->first();
+            if ($driver) {
+                $channels[] = $this->buildChannelFromRecord($driver);
+            }
+        }
+        return $channels;
+    }
+
+    /**
+     * Given details from alphabeticBrowse(), create channel contents.
+     *
+     * @param array $details Details from alphabetic browse index
+     *
+     * @return array
+     */
+    protected function summarizeBrowseDetails($details)
+    {
+        $ids = $results = [];
+        if (isset($details['Browse']['items'])) {
+            foreach ($details['Browse']['items'] as $item) {
+                if (!isset($item['extras']['title'][0][0])) {
+                    continue;
+                }
+                // Collect a list of IDs in the result set while we create it:
+                $ids[] = $id = $item['extras']['id'][0][0];
+                $results[] = [
+                    'title' => $item['extras']['title'][0][0],
+                    'source' => 'Solr',
+                    'thumbnail' => false, // TODO: better thumbnails!
+                    'id' => $id
+                ];
+            }
+        }
+        // If we have a cover router and a non-empty ID list, look up thumbnails:
+        if ($this->coverRouter && !empty($ids)) {
+            $records = $this->searchService->retrieveBatch('Solr', $ids);
+            $thumbs = [];
+            // First map record drivers to an ID => thumb array...
+            foreach ($records as $record) {
+                $thumbs[$record->getUniqueId()] = $this->coverRouter
+                    ->getUrl($record, 'medium');
+            }
+            // Now apply the thumbnails to the existing result set...
+            foreach ($results as $i => $current) {
+                if (isset($thumbs[$current['id']])) {
+                    $results[$i]['thumbnail'] = $thumbs[$current['id']];
+                }
+            }
+        }
+        return $results;
+    }
+
+    /**
+     * Add a new filter to an existing search results object to populate a
+     * channel.
+     *
+     * @param RecordDriver $driver    Record driver
+     * @param bool         $tokenOnly Create full channel (false) or return a
+     * token for future loading (true)?
+     *
+     * @return array
+     */
+    protected function buildChannelFromRecord(RecordDriver $driver,
+        $tokenOnly = false
+    ) {
+        $retVal = [
+            'title' => $this->translate(
+                'nearby_items', ['%%title%%' => $driver->getBreadcrumb()]
+            ),
+            'providerId' => $this->providerId,
+            'links' => []
+        ];
+        $raw = $driver->getRawData();
+        $from = isset($raw[$this->solrField]) ? (array)$raw[$this->solrField] : null;
+        if (empty($from[0])) {
+            // If there is no "from" value to look up, skip this so we don't
+            //generate a token that retrieves nothing later!
+            $retVal['contents'] = [];
+        } elseif ($tokenOnly) {
+            $retVal['token'] = $driver->getUniqueID();
+        } else {
+            // If we got this far, we can safely assume that $from[0] is set
+            $details = $this->solr->alphabeticBrowse(
+                $this->browseIndex, $from[0], 0, $this->channelSize,
+                new ParamBag(['extras' => 'title:author:isbn:id']),
+                -$this->rowsBefore
+            );
+            $retVal['contents'] = $this->summarizeBrowseDetails($details);
+            $route = $this->recordRouter->getRouteDetails($driver);
+            $retVal['links'][] = [
+                'label' => 'View Record',
+                'icon' => 'fa-file-text-o',
+                'url' => $this->url
+                    ->fromRoute($route['route'], $route['params'])
+            ];
+            $retVal['links'][] = [
+                'label' => 'channel_expand',
+                'icon' => 'fa-search-plus',
+                'url' => $this->url->fromRoute('channels-record')
+                    . '?id=' . urlencode($driver->getUniqueID())
+                    . '&source=' . urlencode($driver->getSourceIdentifier())
+            ];
+            $retVal['links'][] = [
+                'label' => 'channel_browse',
+                'icon' => 'fa-list',
+                'url' => $this->url->fromRoute('alphabrowse-home')
+                    . '?source=' . urlencode($this->browseIndex)
+                    . '&from=' . $from[0]
+            ];
+        }
+        return $retVal;
+    }
+}
diff --git a/module/VuFind/src/VuFind/ChannelProvider/Factory.php b/module/VuFind/src/VuFind/ChannelProvider/Factory.php
index 413803f34c6184486e5aa1bf344eb710b85652fc..25a1b96172b158a006e64828ad2506054e04a025 100644
--- a/module/VuFind/src/VuFind/ChannelProvider/Factory.php
+++ b/module/VuFind/src/VuFind/ChannelProvider/Factory.php
@@ -41,6 +41,28 @@ use Zend\ServiceManager\ServiceManager;
  */
 class Factory
 {
+    /**
+     * Construct the AlphaBrowse channel provider.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return AlphaBrowse
+     */
+    public static function getAlphaBrowse(ServiceManager $sm)
+    {
+        $helper = new AlphaBrowse(
+            $sm->getServiceLocator()->get('VuFind\Search'),
+            $sm->getServiceLocator()->get('VuFind\Search\BackendManager')
+                ->get('Solr'),
+            $sm->getServiceLocator()->get('ControllerPluginManager')->get('url'),
+            $sm->getServiceLocator()->get('VuFind\RecordRouter')
+        );
+        $helper->setCoverRouter(
+            $sm->getServiceLocator()->get('VuFind\Cover\Router')
+        );
+        return $helper;
+    }
+
     /**
      * Construct the Facets channel provider.
      *
diff --git a/themes/bootstrap3/templates/channels/channelList.phtml b/themes/bootstrap3/templates/channels/channelList.phtml
index b2e9f6008073325e497f8b84a9edf0b320cfce43..788851f5d47e76fc1b33ccd20d657b1064d86bc0 100644
--- a/themes/bootstrap3/templates/channels/channelList.phtml
+++ b/themes/bootstrap3/templates/channels/channelList.phtml
@@ -4,6 +4,7 @@
 <? $this->headScript()->appendFile('vendor/jquery.dotdotdot.min.js'); ?>
 <?
   $this->jsTranslations()->addStrings([
+    'channel_browse' => 'channel_browse',
     'channel_expand' => 'channel_expand',
     'channel_search' => 'channel_search',
     'nohit_heading' => 'nohit_heading',