[open-ils-commits] [GIT] Evergreen ILS branch master updated. a816a7136c1947d6bda773dae30647e102849a55

Evergreen Git git at git.evergreen-ils.org
Fri Oct 14 14:22:23 EDT 2011


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  a816a7136c1947d6bda773dae30647e102849a55 (commit)
       via  4ac646952b8c40fc8cf445e77e76287b28da36b8 (commit)
       via  40a4b6e6a7784eedd80c8ca968ee2316314f54d0 (commit)
       via  399a5cc724ee3afce781f7e9dc1b50bb570a952b (commit)
       via  52fc7f08004508d0355f93dd7c4c51cc193b0513 (commit)
      from  a97ede007d7928117b734a43acd3d12343795e22 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit a816a7136c1947d6bda773dae30647e102849a55
Author: Bill Erickson <berick at esilibrary.com>
Date:   Fri Oct 14 14:20:56 2011 -0400

    Stamped upgrade scrip fro authorities-indb-with-nfi
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index f8f9baa..946150d 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -86,7 +86,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0639', :eg_version); -- dbs/miker
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0640', :eg_version); -- miker/lebbeous/berick
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema-acs-nfi.sql b/Open-ILS/src/sql/Pg/upgrade/0640.schema-acs-nfi.sql
similarity index 99%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema-acs-nfi.sql
rename to Open-ILS/src/sql/Pg/upgrade/0640.schema-acs-nfi.sql
index 86b2ec4..89430aa 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema-acs-nfi.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/0640.schema-acs-nfi.sql
@@ -1,7 +1,7 @@
 -- XXXX.schema-acs-nfi.sql
 BEGIN;
 
-SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('0640', :eg_version);
 
 -- AFTER UPDATE OR INSERT trigger for authority.record_entry
 CREATE OR REPLACE FUNCTION authority.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$

commit 4ac646952b8c40fc8cf445e77e76287b28da36b8
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Oct 11 14:25:36 2011 -0400

    Move the NACO normalization script to earlier in the baseline schema installation process, as it is now used in the authority schema
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Bill Erickson <berick at esilibrary.com>

diff --git a/Open-ILS/src/sql/Pg/002.functions.config.sql b/Open-ILS/src/sql/Pg/002.functions.config.sql
index 353c7f2..4836384 100644
--- a/Open-ILS/src/sql/Pg/002.functions.config.sql
+++ b/Open-ILS/src/sql/Pg/002.functions.config.sql
@@ -635,5 +635,77 @@ CREATE OR REPLACE FUNCTION evergreen.lpad_number_substrings( TEXT, TEXT, INT ) R
     return $string;
 $$ LANGUAGE PLPERLU;
 
+CREATE OR REPLACE FUNCTION public.naco_normalize( TEXT, TEXT ) RETURNS TEXT AS $func$
+
+    use strict;
+    use Unicode::Normalize;
+    use Encode;
+
+    my $str = decode_utf8(shift);
+    my $sf = shift;
+
+    # Apply NACO normalization to input string; based on
+    # http://www.loc.gov/catdir/pcc/naco/SCA_PccNormalization_Final_revised.pdf
+    #
+    # Note that unlike a strict reading of the NACO normalization rules,
+    # output is returned as lowercase instead of uppercase for compatibility
+    # with previous versions of the Evergreen naco_normalize routine.
+
+    # Convert to upper-case first; even though final output will be lowercase, doing this will
+    # ensure that the German eszett (ß) and certain ligatures (ff, fi, ffl, etc.) will be handled correctly.
+    # If there are any bugs in Perl's implementation of upcasing, they will be passed through here.
+    $str = uc $str;
+
+    # remove non-filing strings
+    $str =~ s/\x{0098}.*?\x{009C}//g;
+
+    $str = NFKD($str);
+
+    # additional substitutions - 3.6.
+    $str =~ s/\x{00C6}/AE/g;
+    $str =~ s/\x{00DE}/TH/g;
+    $str =~ s/\x{0152}/OE/g;
+    $str =~ tr/\x{0110}\x{00D0}\x{00D8}\x{0141}\x{2113}\x{02BB}\x{02BC}]['/DDOLl/d;
+
+    # transformations based on Unicode category codes
+    $str =~ s/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Lm}\p{Mc}\p{Me}\p{Mn}]//g;
+
+	if ($sf && $sf =~ /^a/o) {
+		my $commapos = index($str, ',');
+		if ($commapos > -1) {
+			if ($commapos != length($str) - 1) {
+                $str =~ s/,/\x07/; # preserve first comma
+			}
+		}
+	}
+
+    # since we've stripped out the control characters, we can now
+    # use a few as placeholders temporarily
+    $str =~ tr/+&@\x{266D}\x{266F}#/\x01\x02\x03\x04\x05\x06/;
+    $str =~ s/[\p{Pc}\p{Pd}\p{Pe}\p{Pf}\p{Pi}\p{Po}\p{Ps}\p{Sk}\p{Sm}\p{So}\p{Zl}\p{Zp}\p{Zs}]/ /g;
+    $str =~ tr/\x01\x02\x03\x04\x05\x06\x07/+&@\x{266D}\x{266F}#,/;
+
+    # decimal digits
+    $str =~ tr/\x{0660}-\x{0669}\x{06F0}-\x{06F9}\x{07C0}-\x{07C9}\x{0966}-\x{096F}\x{09E6}-\x{09EF}\x{0A66}-\x{0A6F}\x{0AE6}-\x{0AEF}\x{0B66}-\x{0B6F}\x{0BE6}-\x{0BEF}\x{0C66}-\x{0C6F}\x{0CE6}-\x{0CEF}\x{0D66}-\x{0D6F}\x{0E50}-\x{0E59}\x{0ED0}-\x{0ED9}\x{0F20}-\x{0F29}\x{1040}-\x{1049}\x{1090}-\x{1099}\x{17E0}-\x{17E9}\x{1810}-\x{1819}\x{1946}-\x{194F}\x{19D0}-\x{19D9}\x{1A80}-\x{1A89}\x{1A90}-\x{1A99}\x{1B50}-\x{1B59}\x{1BB0}-\x{1BB9}\x{1C40}-\x{1C49}\x{1C50}-\x{1C59}\x{A620}-\x{A629}\x{A8D0}-\x{A8D9}\x{A900}-\x{A909}\x{A9D0}-\x{A9D9}\x{AA50}-\x{AA59}\x{ABF0}-\x{ABF9}\x{FF10}-\x{FF19}/0-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-9/;
+
+    # intentionally skipping step 8 of the NACO algorithm; if the string
+    # gets normalized away, that's fine.
+
+    # leading and trailing spaces
+    $str =~ s/\s+/ /g;
+    $str =~ s/^\s+//;
+    $str =~ s/\s+$//g;
+
+    return lc $str;
+$func$ LANGUAGE 'plperlu' STRICT IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION public.naco_normalize_keep_comma( TEXT ) RETURNS TEXT AS $func$
+        SELECT public.naco_normalize($1,'a');
+$func$ LANGUAGE SQL STRICT IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION public.naco_normalize( TEXT ) RETURNS TEXT AS $func$
+	SELECT public.naco_normalize($1,'');
+$func$ LANGUAGE 'sql' STRICT IMMUTABLE;
+
 COMMIT;
 
diff --git a/Open-ILS/src/sql/Pg/020.schema.functions.sql b/Open-ILS/src/sql/Pg/020.schema.functions.sql
index 8c6a0c8..ca4306e 100644
--- a/Open-ILS/src/sql/Pg/020.schema.functions.sql
+++ b/Open-ILS/src/sql/Pg/020.schema.functions.sql
@@ -33,82 +33,10 @@ CREATE OR REPLACE FUNCTION public.non_filing_normalize ( TEXT, "char" ) RETURNS
 		);
 $$ LANGUAGE SQL STRICT IMMUTABLE;
 
-CREATE OR REPLACE FUNCTION public.naco_normalize( TEXT, TEXT ) RETURNS TEXT AS $func$
-
-    use strict;
-    use Unicode::Normalize;
-    use Encode;
-
-    my $str = decode_utf8(shift);
-    my $sf = shift;
-
-    # Apply NACO normalization to input string; based on
-    # http://www.loc.gov/catdir/pcc/naco/SCA_PccNormalization_Final_revised.pdf
-    #
-    # Note that unlike a strict reading of the NACO normalization rules,
-    # output is returned as lowercase instead of uppercase for compatibility
-    # with previous versions of the Evergreen naco_normalize routine.
-
-    # Convert to upper-case first; even though final output will be lowercase, doing this will
-    # ensure that the German eszett (ß) and certain ligatures (ff, fi, ffl, etc.) will be handled correctly.
-    # If there are any bugs in Perl's implementation of upcasing, they will be passed through here.
-    $str = uc $str;
-
-    # remove non-filing strings
-    $str =~ s/\x{0098}.*?\x{009C}//g;
-
-    $str = NFKD($str);
-
-    # additional substitutions - 3.6.
-    $str =~ s/\x{00C6}/AE/g;
-    $str =~ s/\x{00DE}/TH/g;
-    $str =~ s/\x{0152}/OE/g;
-    $str =~ tr/\x{0110}\x{00D0}\x{00D8}\x{0141}\x{2113}\x{02BB}\x{02BC}]['/DDOLl/d;
-
-    # transformations based on Unicode category codes
-    $str =~ s/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Lm}\p{Mc}\p{Me}\p{Mn}]//g;
-
-	if ($sf && $sf =~ /^a/o) {
-		my $commapos = index($str, ',');
-		if ($commapos > -1) {
-			if ($commapos != length($str) - 1) {
-                $str =~ s/,/\x07/; # preserve first comma
-			}
-		}
-	}
-
-    # since we've stripped out the control characters, we can now
-    # use a few as placeholders temporarily
-    $str =~ tr/+&@\x{266D}\x{266F}#/\x01\x02\x03\x04\x05\x06/;
-    $str =~ s/[\p{Pc}\p{Pd}\p{Pe}\p{Pf}\p{Pi}\p{Po}\p{Ps}\p{Sk}\p{Sm}\p{So}\p{Zl}\p{Zp}\p{Zs}]/ /g;
-    $str =~ tr/\x01\x02\x03\x04\x05\x06\x07/+&@\x{266D}\x{266F}#,/;
-
-    # decimal digits
-    $str =~ tr/\x{0660}-\x{0669}\x{06F0}-\x{06F9}\x{07C0}-\x{07C9}\x{0966}-\x{096F}\x{09E6}-\x{09EF}\x{0A66}-\x{0A6F}\x{0AE6}-\x{0AEF}\x{0B66}-\x{0B6F}\x{0BE6}-\x{0BEF}\x{0C66}-\x{0C6F}\x{0CE6}-\x{0CEF}\x{0D66}-\x{0D6F}\x{0E50}-\x{0E59}\x{0ED0}-\x{0ED9}\x{0F20}-\x{0F29}\x{1040}-\x{1049}\x{1090}-\x{1099}\x{17E0}-\x{17E9}\x{1810}-\x{1819}\x{1946}-\x{194F}\x{19D0}-\x{19D9}\x{1A80}-\x{1A89}\x{1A90}-\x{1A99}\x{1B50}-\x{1B59}\x{1BB0}-\x{1BB9}\x{1C40}-\x{1C49}\x{1C50}-\x{1C59}\x{A620}-\x{A629}\x{A8D0}-\x{A8D9}\x{A900}-\x{A909}\x{A9D0}-\x{A9D9}\x{AA50}-\x{AA59}\x{ABF0}-\x{ABF9}\x{FF10}-\x{FF19}/0-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-9/;
-
-    # intentionally skipping step 8 of the NACO algorithm; if the string
-    # gets normalized away, that's fine.
-
-    # leading and trailing spaces
-    $str =~ s/\s+/ /g;
-    $str =~ s/^\s+//;
-    $str =~ s/\s+$//g;
-
-    return lc $str;
-$func$ LANGUAGE 'plperlu' STRICT IMMUTABLE;
-
-CREATE OR REPLACE FUNCTION public.naco_normalize( TEXT ) RETURNS TEXT AS $func$
-	SELECT public.naco_normalize($1,'');
-$func$ LANGUAGE 'sql' STRICT IMMUTABLE;
-
 CREATE OR REPLACE FUNCTION public.first_word ( TEXT ) RETURNS TEXT AS $$
         SELECT COALESCE(SUBSTRING( $1 FROM $_$^\S+$_$), '');
 $$ LANGUAGE SQL STRICT IMMUTABLE;
 
-CREATE OR REPLACE FUNCTION public.naco_normalize_keep_comma( TEXT ) RETURNS TEXT AS $func$
-        SELECT public.naco_normalize($1,'a');
-$func$ LANGUAGE SQL STRICT IMMUTABLE;
-
 CREATE OR REPLACE FUNCTION public.normalize_space( TEXT ) RETURNS TEXT AS $$
     SELECT regexp_replace(regexp_replace(regexp_replace($1, E'\\n', ' ', 'g'), E'(?:^\\s+)|(\\s+$)', '', 'g'), E'\\s+', ' ', 'g');
 $$ LANGUAGE SQL STRICT IMMUTABLE;

commit 40a4b6e6a7784eedd80c8ca968ee2316314f54d0
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Oct 7 16:22:42 2011 -0400

    Wrapper functions in the baseline schema that were missed in the upgrade script
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Bill Erickson <berick at esilibrary.com>

diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema-acs-nfi.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema-acs-nfi.sql
index 1e5e85a..86b2ec4 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema-acs-nfi.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema-acs-nfi.sql
@@ -136,6 +136,14 @@ BEGIN
 END;
 $func$ LANGUAGE PLPGSQL IMMUTABLE;
 
+CREATE OR REPLACE FUNCTION authority.simple_normalize_heading( marcxml TEXT ) RETURNS TEXT AS $func$
+    SELECT authority.normalize_heading($1, TRUE);
+$func$ LANGUAGE SQL IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION authority.normalize_heading( marcxml TEXT ) RETURNS TEXT AS $func$
+    SELECT authority.normalize_heading($1, FALSE);
+$func$ LANGUAGE SQL IMMUTABLE;
+
 
 CREATE TABLE authority.simple_heading (
     id              BIGSERIAL   PRIMARY KEY,

commit 399a5cc724ee3afce781f7e9dc1b50bb570a952b
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Oct 7 10:15:19 2011 -0400

    Address regression causing main headings to be null because the thesaurus code we not being extracted as it was before
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Bill Erickson <berick at esilibrary.com>

diff --git a/Open-ILS/src/sql/Pg/011.schema.authority.sql b/Open-ILS/src/sql/Pg/011.schema.authority.sql
index 09bd745..4f8fc74 100644
--- a/Open-ILS/src/sql/Pg/011.schema.authority.sql
+++ b/Open-ILS/src/sql/Pg/011.schema.authority.sql
@@ -168,7 +168,10 @@ BEGIN
           LIMIT 1;
     END IF;
 
-    IF thes_code = 'z' THEN
+    thes_code := vandelay.marc21_extract_fixed_field(marcxml,'Subj');
+    IF thes_code IS NULL THEN
+        thes_code := '|';
+    ELSIF thes_code = 'z' THEN
         thes_code := COALESCE( oils_xpath_string('//*[@tag="040"]/*[@code="f"][1]', marcxml), '' );
     END IF;
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema-acs-nfi.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema-acs-nfi.sql
index 9f5762f..1e5e85a 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema-acs-nfi.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema-acs-nfi.sql
@@ -78,7 +78,10 @@ BEGIN
           LIMIT 1;
     END IF;
 
-    IF thes_code = 'z' THEN
+    thes_code := vandelay.marc21_extract_fixed_field(marcxml,'Subj');
+    IF thes_code IS NULL THEN
+        thes_code := '|';
+    ELSIF thes_code = 'z' THEN
         thes_code := COALESCE( oils_xpath_string('//*[@tag="040"]/*[@code="f"][1]', marcxml), '' );
     END IF;
 

commit 52fc7f08004508d0355f93dd7c4c51cc193b0513
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Sep 21 16:38:08 2011 -0400

    New in-db search and browse of authorities with non-filing-indicator support
    
    Upon authority ingest, record simple authority headings for searching,
    browsing and sorting.  Respect non-filing indicators, if configured
    (configuration is the nfi field on authority.control_set_authority_field).
    
    Provide a full suite of search/browse top/center alpha/rank functions
    based on authority tag, bib tag or browse axis at the database layer.
    
    Provide functions to find the main-only and full tag list, and ref-variant
    versions of the previously defined functions.
    
    Teach OpenILS::WWW::SuperCat (responsible for RESTful feed-generating
    interfaces) to use new OpenILS::Application::SuperCat methods that in
    turn use the database methods.  The change should be transparent to
    users of SuperCat web APIs.
    
    Carry indicators over from authority main entries to bibs, when applying
    them in the marc editor.
    
    Signed-off-by: Lebbeous Fogle-Weekley <lebbeous at esilibrary.com>
    Signed-off-by: Bill Erickson <berick at esilibrary.com>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index f205fd9..fae1de5 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -1789,6 +1789,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 			<field reporter:label="Control Set" name="control_set" reporter:datatype="link"/>
 			<field reporter:label="Tag" name="tag" reporter:datatype="text" oils_obj:required="true" oils_obj:validate="^.{3}$"/>
 			<field reporter:label="Subfield List" name="sf_list" reporter:datatype="text" />
+			<field reporter:label="Non-filing Indicator" name="nfi" reporter:datatype="text" />
 			<field reporter:label="Name" name="name" reporter:datatype="text" oils_persist:i18n="true" oils_obj:required="true" />
 			<field reporter:label="Description" name="description" reporter:datatype="text" oils_persist:i18n="true" />
 			<field reporter:label="Subordinate Entries" name="sub_entries" reporter:datatype="link" oils_persist:virtual="true"/>
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/authority.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/authority.pm
index af0dff6..b0cdfe4 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/authority.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/authority.pm
@@ -259,6 +259,51 @@ for my $class ( qw/title author subject keyword series identifier/ ) {
 		cachable	=> 1,
 	);
 }
+__PACKAGE__->register_method(
+    api_name	=> "open-ils.storage.authority.in_db.browse_or_search",
+    method		=> "authority_in_db_browse_or_search",
+    api_level	=> 1,
+    argc        => 5,
+    signature   => {
+        desc => q/Use stored procedures to perform authorities-based
+        browses or searches/,
+        params => [
+            {name => "method", type => "string", desc => q/
+                The name of a method within the authority schema to call.  This
+                is an API call on a private service for a reason.  Do not pass
+                unfiltered user input into this API call, especially in this
+                parameter./},
+            {name => "what", type => "string", desc => q/
+                What to search. Could be an axis name, an authority tag
+                number, or a bib tag number/},
+            {name => "term", type => "string", desc => "Search term"},
+            {name => "page", type => "number", desc => "Zero-based page number"},
+            {name => "page_size", type => "number",
+                desc => "Number of records per page"}
+        ],
+        return => {
+            desc => "A list of authority record IDs",
+            type => "array"
+        }
+    }
+);
+
+sub authority_in_db_browse_or_search {
+    my ($self, $shift, $method, @args) = @_;
 
+    return unless $method =~ /^\w+$/;
+
+    my $db = authority::full_rec->db_Main;
+	my $list = $db->selectcol_arrayref(
+        qq/
+            SELECT
+                (SELECT record FROM authority.simple_heading WHERE id = func.heading)
+            FROM authority.$method(?, ?, ?, ?) func(heading)
+        /,
+        {}, @args
+    );
+
+    return $list;
+}
 
 1;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/SuperCat.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/SuperCat.pm
index a43abc5..879ba77 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/SuperCat.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/SuperCat.pm
@@ -179,6 +179,8 @@ sub child_init {
 
 	register_record_transforms();
 
+	register_new_authorities_methods();
+
 	return 1;
 }
 
@@ -226,6 +228,90 @@ sub register_record_transforms {
 	}
 }
 
+sub register_new_authorities_methods {
+    my %register_args = (
+        method    => "generic_new_authorities_method",
+        api_level => 1,
+        argc      => 1,
+        signature => {
+            desc => q/Generated method/,
+            params => [
+                {name => "what",
+                    desc => "An axis, an authority tag number, or a bibliographic tag number, depending on invocation",
+                    type => "string"},
+                {name => "term",
+                    desc => "A search term",
+                    type => "string"},
+                {name => "page",
+                    desc => "zero-based page number of results",
+                    type => "number"},
+                {name => "page size",
+                    desc => "number of results per page",
+                    type => "number"}
+            ],
+            return => {
+                desc => "A list of authority record IDs", type => "array"
+            }
+        }
+    );
+
+    foreach my $how (qw/axis atag btag/) {
+        foreach my $action (qw/browse_center browse_top
+            search_rank search_heading/) {
+
+            $register_args{api_name} =
+                "open-ils.supercat.authority.$action.by_$how";
+            __PACKAGE__->register_method(%register_args);
+
+            $register_args{api_name} =
+                "open-ils.supercat.authority.$action.by_$how.refs";
+            __PACKAGE__->register_method(%register_args);
+
+        }
+    }
+}
+
+sub generic_new_authorities_method {
+    my $self = shift;
+    my $client = shift;
+
+    # We want to be extra careful with these arguments, since the next
+    # thing we're doing with them is passing them to a DB procedure.
+    my $what = ''.shift;
+    my $term = ''.shift;
+    my $page = int(shift || 0);
+    my $page_size = shift;
+
+    # undef ok, but other non numbers not ok
+    $page_size = int($page_size) if defined $page_size;
+
+    # Figure out how we were called and what DB procedure we'll call in turn.
+    $self->api_name =~ /\.by_(\w+)($|\.)/;
+    my $metaaxis = $1;
+    my $refs = $2;
+
+    $self->api_name =~ /authority\.(\w+)\./;
+    my $action = $1;
+
+    my $method = "${metaaxis}_$action";
+    $method .= "_refs" if $refs;
+
+    # Match authority.full_rec normalization
+    # XXX don't know whether we need second arg 'subfield'?
+    $term = naco_normalize($term);
+
+    my $storage = create OpenSRF::AppSession("open-ils.storage");
+    my $list = $storage->request(
+        "open-ils.storage.authority.in_db.browse_or_search",
+        $method, $what, $term, $page, $page_size
+    )->gather(1);
+
+    $storage->kill_me;
+
+    return $list;
+}
+
+
 sub tree_walker {
 	my $tree = shift;
 	my $field = shift;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
index 4ca4b9d..708cc22 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
@@ -36,6 +36,8 @@ my $U = 'OpenILS::Application::AppUtils';
 # set the bootstrap config when this module is loaded
 my ($bootstrap, $supercat, $actor, $parser, $search, $xslt, $cn_browse_xslt, %browse_types, %qualifier_map);
 
+my $authority_axis_re = qr/^authority\.(\w+)(\.refs)?$/;
+
 $browse_types{call_number}{xml} = sub {
     my $tree = shift;
 
@@ -1561,12 +1563,17 @@ sub string_browse {
 
     my $tree;
     if ($axis =~ /^authority/) {
+        my ($realaxis, $refs) = ($axis =~ $authority_axis_re);
+
+        my $method = "open-ils.supercat.authority.browse_center.by_axis";
+        $method .= ".refs" if $refs;
+
         $tree = $supercat->request(
-            "open-ils.supercat.authority.browse.by_axis",
-            $axis,
+            $method,
+            $realaxis,
             $string,
-            $page_size,
-            $page
+            $page,
+            $page_size
         )->gather(1);
     } else {
         $tree = $supercat->request(
@@ -1637,12 +1644,17 @@ sub string_startwith {
 
     my $tree;
     if ($axis =~ /^authority/) {
+        my ($realaxis, $refs) = ($axis =~ $authority_axis_re);
+
+        my $method = "open-ils.supercat.authority.browse_top.by_axis";
+        $method .= ".refs" if $refs;
+
         $tree = $supercat->request(
-            "open-ils.supercat.authority.startwith.by_axis",
-            $axis,
+            $method,
+            $realaxis,
             $string,
-            $page_size,
-            $page
+            $page,
+            $page_size
         )->gather(1);
     } else {
         $tree = $supercat->request(
@@ -2110,12 +2122,17 @@ sub return_auth_response {
     if ($qualifier eq "id") {
         $recs = [ int($term) ];
     } else {
+        my ($realaxis, $refs) = ($qualifier =~ $authority_axis_re);
+
+        my $method = "open-ils.supercat.authority.browse_top.by_axis";
+        $method .= ".refs" if $refs;
+
         $recs = $supercat->request(
-            "open-ils.supercat.authority.startwith.by_axis",
-            $qualifier,
+            $method,
+            $realaxis,
             $term,
-            $page_size,
-            $page
+            $page,
+            $page_size
         )->gather(1);
     }
 
diff --git a/Open-ILS/src/sql/Pg/011.schema.authority.sql b/Open-ILS/src/sql/Pg/011.schema.authority.sql
index 329f356..09bd745 100644
--- a/Open-ILS/src/sql/Pg/011.schema.authority.sql
+++ b/Open-ILS/src/sql/Pg/011.schema.authority.sql
@@ -33,6 +33,7 @@ CREATE TABLE authority.control_set_authority_field (
     main_entry  INT     REFERENCES authority.control_set_authority_field (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
     control_set INT     NOT NULL REFERENCES authority.control_set (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
     tag         CHAR(3) NOT NULL,
+    nfi         CHAR(1),          -- non-filing indicator
     sf_list     TEXT    NOT NULL,
     name        TEXT    NOT NULL, -- i18n
     description TEXT              -- i18n
@@ -131,7 +132,7 @@ CREATE INDEX authority_full_rec_tag_part_idx ON authority.full_rec (SUBSTRING(ta
 CREATE INDEX authority_full_rec_subfield_a_idx ON authority.full_rec (value) WHERE subfield = 'a';
 CREATE TRIGGER authority_full_rec_fti_trigger
     BEFORE UPDATE OR INSERT ON authority.full_rec
-    FOR EACH ROW EXECUTE PROCEDURE tsearch2(index_vector, value);
+    FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
 
 CREATE INDEX authority_full_rec_index_vector_idx ON authority.full_rec USING GIST (index_vector);
 /* Enable LIKE to use an index for database clusters with locales other than C or POSIX */
@@ -149,27 +150,58 @@ CREATE OR REPLACE FUNCTION authority.normalize_heading( marcxml TEXT, no_thesaur
 DECLARE
     acsaf           authority.control_set_authority_field%ROWTYPE;
     tag_used        TEXT;
+    nfi_used        TEXT;
     sf              TEXT;
     thes_code       TEXT;
     cset            INT;
     heading_text    TEXT;
     tmp_text        TEXT;
+    first_sf        BOOL;
+    auth_id         INT DEFAULT oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', marcxml)::INT;
 BEGIN
-    thes_code := vandelay.marc21_extract_fixed_field(marcxml,'Subj');
-    IF thes_code IS NULL THEN
-        thes_code := '|';
+    SELECT control_set INTO cset FROM authority.record_entry WHERE id = auth_id;
+
+    IF cset IS NULL THEN
+        SELECT  control_set INTO cset
+          FROM  authority.control_set_authority_field
+          WHERE tag IN ( SELECT  UNNEST(XPATH('//*[starts-with(@tag,"1")]/@tag',marcxml::XML)::TEXT[]))
+          LIMIT 1;
     END IF;
 
-    SELECT control_set INTO cset FROM authority.thesaurus WHERE code = thes_code;
-    IF NOT FOUND THEN
-        cset = 1;
+    IF thes_code = 'z' THEN
+        thes_code := COALESCE( oils_xpath_string('//*[@tag="040"]/*[@code="f"][1]', marcxml), '' );
     END IF;
 
     heading_text := '';
     FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset AND main_entry IS NULL LOOP
         tag_used := acsaf.tag;
+        nfi_used := acsaf.nfi;
+        first_sf := TRUE;
         FOR sf IN SELECT * FROM regexp_split_to_table(acsaf.sf_list,'') LOOP
             tmp_text := oils_xpath_string('//*[@tag="'||tag_used||'"]/*[@code="'||sf||'"]', marcxml);
+
+            IF first_sf AND tmp_text IS NOT NULL AND nfi_used IS NOT NULL THEN
+
+                tmp_text := SUBSTRING(
+                    tmp_text FROM
+                    COALESCE(
+                        NULLIF(
+                            REGEXP_REPLACE(
+                                oils_xpath_string('//*[@tag="'||tag_used||'"]/@ind'||nfi_used, marcxml),
+                                $$\D+$$,
+                                '',
+                                'g'
+                            ),
+                            ''
+                        )::INT,
+                        0
+                    ) + 1
+                );
+
+            END IF;
+
+            first_sf := FALSE;
+
             IF tmp_text IS NOT NULL AND tmp_text <> '' THEN
                 heading_text := heading_text || E'\u2021' || sf || ' ' || tmp_text;
             END IF;
@@ -177,15 +209,11 @@ BEGIN
         EXIT WHEN heading_text <> '';
     END LOOP;
 
-    IF thes_code = 'z' THEN
-        thes_code := oils_xpath_string('//*[@tag="040"]/*[@code="f"][1]', marcxml);
-    END IF;
-
     IF heading_text <> '' THEN
         IF no_thesaurus IS TRUE THEN
             heading_text := tag_used || ' ' || public.naco_normalize(heading_text);
         ELSE
-            heading_text := tag_used || '_' || thes_code || ' ' || public.naco_normalize(heading_text);
+            heading_text := tag_used || '_' || COALESCE(nfi_used,'-') || '_' || thes_code || ' ' || public.naco_normalize(heading_text);
         END IF;
     ELSE
         heading_text := 'NOHEADING_' || thes_code || ' ' || MD5(marcxml);
@@ -195,6 +223,96 @@ BEGIN
 END;
 $func$ LANGUAGE PLPGSQL IMMUTABLE;
 
+CREATE TABLE authority.simple_heading (
+    id              BIGSERIAL   PRIMARY KEY,
+    record          BIGINT      NOT NULL REFERENCES authority.record_entry (id),
+    atag            INT         NOT NULL REFERENCES authority.control_set_authority_field (id),
+    value           TEXT        NOT NULL,
+    sort_value      TEXT        NOT NULL,
+    index_vector    tsvector    NOT NULL
+);
+CREATE TRIGGER authority_simple_heading_fti_trigger
+    BEFORE UPDATE OR INSERT ON authority.simple_heading
+    FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
+
+CREATE INDEX authority_simple_heading_index_vector_idx ON authority.simple_heading USING GIST (index_vector);
+CREATE INDEX authority_simple_heading_value_idx ON authority.simple_heading (value);
+CREATE INDEX authority_simple_heading_sort_value_idx ON authority.simple_heading (sort_value);
+
+CREATE OR REPLACE FUNCTION authority.simple_heading_set( marcxml TEXT ) RETURNS SETOF authority.simple_heading AS $func$
+DECLARE
+    res             authority.simple_heading%ROWTYPE;
+    acsaf           authority.control_set_authority_field%ROWTYPE;
+    tag_used        TEXT;
+    nfi_used        TEXT;
+    sf              TEXT;
+    cset            INT;
+    heading_text    TEXT;
+    sort_text       TEXT;
+    tmp_text        TEXT;
+    tmp_xml         TEXT;
+    first_sf        BOOL;
+    auth_id         INT DEFAULT oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', marcxml)::INT;
+BEGIN
+
+    res.record := auth_id;
+
+    SELECT  control_set INTO cset
+      FROM  authority.control_set_authority_field
+      WHERE tag IN ( SELECT UNNEST(XPATH('//*[starts-with(@tag,"1")]/@tag',marcxml::XML)::TEXT[]) )
+      LIMIT 1;
+
+    FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset LOOP
+
+        res.atag := acsaf.id;
+        tag_used := acsaf.tag;
+        nfi_used := acsaf.nfi;
+
+        FOR tmp_xml IN SELECT UNNEST(XPATH('//*[@tag="'||tag_used||'"]', marcxml::XML)) LOOP
+            heading_text := '';
+
+            FOR sf IN SELECT * FROM regexp_split_to_table(acsaf.sf_list,'') LOOP
+                heading_text := heading_text || COALESCE( ' ' || oils_xpath_string('//*[@code="'||sf||'"]',tmp_xml::TEXT), '');
+            END LOOP;
+
+            heading_text := public.naco_normalize(heading_text);
+            
+            IF nfi_used IS NOT NULL THEN
+
+                sort_text := SUBSTRING(
+                    heading_text FROM
+                    COALESCE(
+                        NULLIF(
+                            REGEXP_REPLACE(
+                                oils_xpath_string('//*[@tag="'||tag_used||'"]/@ind'||nfi_used, marcxml),
+                                $$\D+$$,
+                                '',
+                                'g'
+                            ),
+                            ''
+                        )::INT,
+                        0
+                    ) + 1
+                );
+
+            ELSE
+                sort_text := heading_text;
+            END IF;
+
+            IF heading_text IS NOT NULL AND heading_text <> '' THEN
+                res.value := heading_text;
+                res.sort_value := sort_text;
+                RETURN NEXT res;
+            END IF;
+
+        END LOOP;
+
+    END LOOP;
+
+    RETURN;
+END;
+$func$ LANGUAGE PLPGSQL IMMUTABLE;
+
 CREATE OR REPLACE FUNCTION authority.simple_normalize_heading( marcxml TEXT ) RETURNS TEXT AS $func$
     SELECT authority.normalize_heading($1, TRUE);
 $func$ LANGUAGE SQL IMMUTABLE;
@@ -385,4 +503,310 @@ BEGIN
 END;
 $func$ LANGUAGE plpgsql;
 
+
+-- Support function used to find the pivot for alpha-heading-browse style searching
+CREATE OR REPLACE FUNCTION authority.simple_heading_find_pivot( a INT[], q TEXT ) RETURNS TEXT AS $$
+DECLARE
+    sort_value_row  RECORD;
+    value_row       RECORD;
+    t_term          TEXT;
+BEGIN
+
+    t_term := public.naco_normalize(q);
+
+    SELECT  CASE WHEN ash.sort_value LIKE t_term || '%' THEN 1 ELSE 0 END
+                + CASE WHEN ash.value LIKE t_term || '%' THEN 1 ELSE 0 END AS rank,
+            ash.sort_value
+      INTO  sort_value_row
+      FROM  authority.simple_heading ash
+      WHERE ash.atag = ANY (a)
+            AND ash.sort_value >= t_term
+      ORDER BY rank DESC, ash.sort_value
+      LIMIT 1;
+
+    SELECT  CASE WHEN ash.sort_value LIKE t_term || '%' THEN 1 ELSE 0 END
+                + CASE WHEN ash.value LIKE t_term || '%' THEN 1 ELSE 0 END AS rank,
+            ash.sort_value
+      INTO  value_row
+      FROM  authority.simple_heading ash
+      WHERE ash.atag = ANY (a)
+            AND ash.value >= t_term
+      ORDER BY rank DESC, ash.sort_value
+      LIMIT 1;
+
+    IF value_row.rank > sort_value_row.rank THEN
+        RETURN value_row.sort_value;
+    ELSE
+        RETURN sort_value_row.sort_value;
+    END IF;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION authority.simple_heading_browse_center( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
+DECLARE
+    pivot_sort_value    TEXT;
+    boffset             INT DEFAULT 0;
+    aoffset             INT DEFAULT 0;
+    blimit              INT DEFAULT 0;
+    alimit              INT DEFAULT 0;
+BEGIN
+
+    pivot_sort_value := authority.simple_heading_find_pivot(atag_list,q);
+
+    IF page = 0 THEN
+        blimit := pagesize / 2;
+        alimit := blimit;
+
+        IF pagesize % 2 <> 0 THEN
+            alimit := alimit + 1;
+        END IF;
+    ELSE
+        blimit := pagesize;
+        alimit := blimit;
+
+        boffset := pagesize / 2;
+        aoffset := boffset;
+
+        IF pagesize % 2 <> 0 THEN
+            boffset := boffset + 1;
+        END IF;
+    END IF;
+
+    IF page <= 0 THEN
+        RETURN QUERY
+            -- "bottom" half of the browse results
+            SELECT id FROM (
+                SELECT  ash.id,
+                        row_number() over ()
+                  FROM  authority.simple_heading ash
+                  WHERE ash.atag = ANY (atag_list)
+                        AND ash.sort_value < pivot_sort_value
+                  ORDER BY ash.sort_value DESC
+                  LIMIT blimit
+                  OFFSET ABS(page) * pagesize - boffset
+            ) x ORDER BY row_number DESC;
+    END IF;
+
+    IF page >= 0 THEN
+        RETURN QUERY
+            -- "bottom" half of the browse results
+            SELECT  ash.id
+              FROM  authority.simple_heading ash
+              WHERE ash.atag = ANY (atag_list)
+                    AND ash.sort_value >= pivot_sort_value
+              ORDER BY ash.sort_value
+              LIMIT alimit
+              OFFSET ABS(page) * pagesize - aoffset;
+    END IF;
+END;
+$$ LANGUAGE PLPGSQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_authority_tags(a TEXT) RETURNS INT[] AS $$
+    SELECT ARRAY_ACCUM(field) FROM authority.browse_axis_authority_field_map WHERE axis = $1;
+$$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION authority.axis_authority_tags_refs(a TEXT) RETURNS INT[] AS $$
+    SELECT  ARRAY_CAT(
+                ARRAY[a.field],
+                (SELECT ARRAY_ACCUM(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.field)
+            )
+      FROM  authority.browse_axis_authority_field_map a
+      WHERE axis = $1
+$$ LANGUAGE SQL;
+
+
+
+CREATE OR REPLACE FUNCTION authority.btag_authority_tags(btag TEXT) RETURNS INT[] AS $$
+    SELECT ARRAY_ACCUM(authority_field) FROM authority.control_set_bib_field WHERE tag = $1
+$$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION authority.btag_authority_tags_refs(btag TEXT) RETURNS INT[] AS $$
+    SELECT  ARRAY_CAT(
+                ARRAY[a.authority_field],
+                (SELECT ARRAY_ACCUM(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.authority_field)
+            )
+      FROM  authority.control_set_bib_field a
+      WHERE a.tag = $1
+$$ LANGUAGE SQL;
+
+
+
+CREATE OR REPLACE FUNCTION authority.atag_authority_tags(atag TEXT) RETURNS INT[] AS $$
+    SELECT ARRAY_ACCUM(id) FROM authority.control_set_authority_field WHERE tag = $1
+$$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION authority.atag_authority_tags_refs(atag TEXT) RETURNS INT[] AS $$
+    SELECT  ARRAY_CAT(
+                ARRAY[a.id],
+                (SELECT ARRAY_ACCUM(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.id)
+            )
+      FROM  authority.control_set_authority_field a
+      WHERE a.tag = $1
+$$ LANGUAGE SQL;
+
+
+
+CREATE OR REPLACE FUNCTION authority.axis_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.axis_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.btag_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.atag_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.axis_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.btag_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.atag_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+
+CREATE OR REPLACE FUNCTION authority.simple_heading_browse_top( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+DECLARE
+    pivot_sort_value    TEXT;
+BEGIN
+
+    pivot_sort_value := authority.simple_heading_find_pivot(atag_list,q);
+
+    IF page < 0 THEN
+        RETURN QUERY
+            -- "bottom" half of the browse results
+            SELECT id FROM (
+                SELECT  ash.id,
+                        row_number() over ()
+                  FROM  authority.simple_heading ash
+                  WHERE ash.atag = ANY (atag_list)
+                        AND ash.sort_value < pivot_sort_value
+                  ORDER BY ash.sort_value DESC
+                  LIMIT pagesize
+                  OFFSET (ABS(page) - 1) * pagesize
+            ) x ORDER BY row_number DESC;
+    END IF;
+
+    IF page >= 0 THEN
+        RETURN QUERY
+            -- "bottom" half of the browse results
+            SELECT  ash.id
+              FROM  authority.simple_heading ash
+              WHERE ash.atag = ANY (atag_list)
+                    AND ash.sort_value >= pivot_sort_value
+              ORDER BY ash.sort_value
+              LIMIT pagesize
+              OFFSET ABS(page) * pagesize ;
+    END IF;
+END;
+$$ LANGUAGE PLPGSQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.axis_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.btag_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.atag_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.axis_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.btag_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.atag_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+
+CREATE OR REPLACE FUNCTION authority.simple_heading_search_rank( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT  ash.id
+      FROM  authority.simple_heading ash,
+            public.naco_normalize($2) t(term),
+            plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
+      WHERE ash.atag = ANY ($1)
+            AND ash.index_vector @@ ptsq.term
+      ORDER BY ts_rank_cd(ash.index_vector,ptsq.term,14)::numeric
+                    + CASE WHEN ash.sort_value LIKE t.term || '%' THEN 2 ELSE 0 END
+                    + CASE WHEN ash.value LIKE t.term || '%' THEN 1 ELSE 0 END DESC
+      LIMIT $4
+      OFFSET $4 * $3;
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.axis_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.btag_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.atag_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.axis_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.btag_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.atag_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+
+CREATE OR REPLACE FUNCTION authority.simple_heading_search_heading( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT  ash.id
+      FROM  authority.simple_heading ash,
+            public.naco_normalize($2) t(term),
+            plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
+      WHERE ash.atag = ANY ($1)
+            AND ash.index_vector @@ ptsq.term
+      ORDER BY ash.sort_value
+      LIMIT $4
+      OFFSET $4 * $3;
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.axis_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.btag_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.atag_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.axis_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.btag_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.atag_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+
 COMMIT;
+
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 7f0263b..637a014 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -10218,13 +10218,19 @@ INSERT INTO authority.control_set (id, name, description) VALUES (
     oils_i18n_gettext('1','Library of Congress standard authority record control semantics','acs','description')
 );
 
+-- Entries that need to respect an NFI
+INSERT INTO authority.control_set_authority_field (id, control_set, main_entry, tag, sf_list, name, nfi) VALUES
+    (4, 1, NULL, '130', 'adfgklmnoprstvxyz', oils_i18n_gettext('4','Heading -- Uniform Title','acsaf','name'), '2'),
+    (24, 1, 4, '530', 'adfgiklmnoprstvwxyz4', oils_i18n_gettext('24','See Also From Tracing -- Uniform Title','acsaf','name'), '2'),
+    (44, 1, 4, '730', 'adfghklmnoprstvwxyz25', oils_i18n_gettext('44','Established Heading Linking Entry -- Uniform Title','acsaf','name'), '2'),
+    (64, 1, 4, '430', 'adfgiklmnoprstvwxyz4', oils_i18n_gettext('64','See Also Tracing -- Uniform Title','acsaf','name'), '2');
+
 INSERT INTO authority.control_set_authority_field (id, control_set, main_entry, tag, sf_list, name) VALUES
 
 -- Main entries
     (1, 1, NULL, '100', 'abcdefklmnopqrstvxyz', oils_i18n_gettext('1','Heading -- Personal Name','acsaf','name')),
     (2, 1, NULL, '110', 'abcdefgklmnoprstvxyz', oils_i18n_gettext('2','Heading -- Corporate Name','acsaf','name')),
     (3, 1, NULL, '111', 'acdefgklnpqstvxyz', oils_i18n_gettext('3','Heading -- Meeting Name','acsaf','name')),
-    (4, 1, NULL, '130', 'adfgklmnoprstvxyz', oils_i18n_gettext('4','Heading -- Uniform Title','acsaf','name')),
     (5, 1, NULL, '150', 'abvxyz', oils_i18n_gettext('5','Heading -- Topical Term','acsaf','name')),
     (6, 1, NULL, '151', 'avxyz', oils_i18n_gettext('6','Heading -- Geographic Name','acsaf','name')),
     (7, 1, NULL, '155', 'avxyz', oils_i18n_gettext('7','Heading -- Genre/Form Term','acsaf','name')),
@@ -10238,7 +10244,6 @@ INSERT INTO authority.control_set_authority_field (id, control_set, main_entry,
     (21, 1, 1, '500', 'abcdefiklmnopqrstvwxyz4', oils_i18n_gettext('21','See Also From Tracing -- Personal Name','acsaf','name')),
     (22, 1, 2, '510', 'abcdefgiklmnoprstvwxyz4', oils_i18n_gettext('22','See Also From Tracing -- Corporate Name','acsaf','name')),
     (23, 1, 3, '511', 'acdefgiklnpqstvwxyz4', oils_i18n_gettext('23','See Also From Tracing -- Meeting Name','acsaf','name')),
-    (24, 1, 4, '530', 'adfgiklmnoprstvwxyz4', oils_i18n_gettext('24','See Also From Tracing -- Uniform Title','acsaf','name')),
     (25, 1, 5, '550', 'abivwxyz4', oils_i18n_gettext('25','See Also From Tracing -- Topical Term','acsaf','name')),
     (26, 1, 6, '551', 'aivwxyz4', oils_i18n_gettext('26','See Also From Tracing -- Geographic Name','acsaf','name')),
     (27, 1, 7, '555', 'aivwxyz4', oils_i18n_gettext('27','See Also From Tracing -- Genre/Form Term','acsaf','name')),
@@ -10252,7 +10257,6 @@ INSERT INTO authority.control_set_authority_field (id, control_set, main_entry,
     (41, 1, 1, '700', 'abcdefghjklmnopqrstvwxyz25', oils_i18n_gettext('41','Established Heading Linking Entry -- Personal Name','acsaf','name')),
     (42, 1, 2, '710', 'abcdefghklmnoprstvwxyz25', oils_i18n_gettext('42','Established Heading Linking Entry -- Corporate Name','acsaf','name')),
     (43, 1, 3, '711', 'acdefghklnpqstvwxyz25', oils_i18n_gettext('43','Established Heading Linking Entry -- Meeting Name','acsaf','name')),
-    (44, 1, 4, '730', 'adfghklmnoprstvwxyz25', oils_i18n_gettext('44','Established Heading Linking Entry -- Uniform Title','acsaf','name')),
     (45, 1, 5, '750', 'abvwxyz25', oils_i18n_gettext('45','Established Heading Linking Entry -- Topical Term','acsaf','name')),
     (46, 1, 6, '751', 'avwxyz25', oils_i18n_gettext('46','Established Heading Linking Entry -- Geographic Name','acsaf','name')),
     (47, 1, 7, '755', 'avwxyz25', oils_i18n_gettext('47','Established Heading Linking Entry -- Genre/Form Term','acsaf','name')),
@@ -10266,7 +10270,6 @@ INSERT INTO authority.control_set_authority_field (id, control_set, main_entry,
     (61, 1, 1, '400', 'abcdefiklmnopqrstvwxyz4', oils_i18n_gettext('61','See Also Tracing -- Personal Name','acsaf','name')),
     (62, 1, 2, '410', 'abcdefgiklmnoprstvwxyz4', oils_i18n_gettext('62','See Also Tracing -- Corporate Name','acsaf','name')),
     (63, 1, 3, '411', 'acdefgiklnpqstvwxyz4', oils_i18n_gettext('63','See Also Tracing -- Meeting Name','acsaf','name')),
-    (64, 1, 4, '430', 'adfgiklmnoprstvwxyz4', oils_i18n_gettext('64','See Also Tracing -- Uniform Title','acsaf','name')),
     (65, 1, 5, '450', 'abivwxyz4', oils_i18n_gettext('65','See Also Tracing -- Topical Term','acsaf','name')),
     (66, 1, 6, '451', 'aivwxyz4', oils_i18n_gettext('66','See Also Tracing -- Geographic Name','acsaf','name')),
     (67, 1, 7, '455', 'aivwxyz4', oils_i18n_gettext('67','See Also Tracing -- Genre/Form Term','acsaf','name')),
diff --git a/Open-ILS/src/sql/Pg/999.functions.global.sql b/Open-ILS/src/sql/Pg/999.functions.global.sql
index ca108f1..a083584 100644
--- a/Open-ILS/src/sql/Pg/999.functions.global.sql
+++ b/Open-ILS/src/sql/Pg/999.functions.global.sql
@@ -1425,6 +1425,7 @@ BEGIN
     IF NEW.deleted IS TRUE THEN -- If this authority is deleted
         DELETE FROM authority.bib_linking WHERE authority = NEW.id; -- Avoid updating fields in bibs that are no longer visible
         DELETE FROM authority.full_rec WHERE record = NEW.id; -- Avoid validating fields against deleted authority records
+        DELETE FROM authority.simple_heading WHERE record = NEW.id;
           -- Should remove matching $0 from controlled fields at the same time?
         RETURN NEW; -- and we're done
     END IF;
@@ -1435,10 +1436,16 @@ BEGIN
         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
             RETURN NEW;
         END IF;
+
         -- Propagate these updates to any linked bib records
         PERFORM authority.propagate_changes(NEW.id) FROM authority.record_entry WHERE id = NEW.id;
+
+        DELETE FROM authority.simple_heading WHERE record = NEW.id;
     END IF;
 
+    INSERT INTO authority.simple_heading (record,atag,value,sort_value)
+        SELECT record, atag, value, sort_value FROM authority.simple_heading_set(NEW.marc);
+
     -- Flatten and insert the afr data
     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_full_rec' AND enabled;
     IF NOT FOUND THEN
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema-acs-nfi.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema-acs-nfi.sql
new file mode 100644
index 0000000..9f5762f
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema-acs-nfi.sql
@@ -0,0 +1,533 @@
+-- XXXX.schema-acs-nfi.sql
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+-- AFTER UPDATE OR INSERT trigger for authority.record_entry
+CREATE OR REPLACE FUNCTION authority.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
+BEGIN
+
+    IF NEW.deleted IS TRUE THEN -- If this authority is deleted
+        DELETE FROM authority.bib_linking WHERE authority = NEW.id; -- Avoid updating fields in bibs that are no longer visible
+        DELETE FROM authority.full_rec WHERE record = NEW.id; -- Avoid validating fields against deleted authority records
+        DELETE FROM authority.simple_heading WHERE record = NEW.id;
+          -- Should remove matching $0 from controlled fields at the same time?
+        RETURN NEW; -- and we're done
+    END IF;
+
+    IF TG_OP = 'UPDATE' THEN -- re-ingest?
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
+
+        IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
+            RETURN NEW;
+        END IF;
+
+        -- Propagate these updates to any linked bib records
+        PERFORM authority.propagate_changes(NEW.id) FROM authority.record_entry WHERE id = NEW.id;
+
+        DELETE FROM authority.simple_heading WHERE record = NEW.id;
+    END IF;
+
+    INSERT INTO authority.simple_heading (record,atag,value,sort_value)
+        SELECT record, atag, value, sort_value FROM authority.simple_heading_set(NEW.marc);
+
+    -- Flatten and insert the afr data
+    PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_full_rec' AND enabled;
+    IF NOT FOUND THEN
+        PERFORM authority.reingest_authority_full_rec(NEW.id);
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_rec_descriptor' AND enabled;
+        IF NOT FOUND THEN
+            PERFORM authority.reingest_authority_rec_descriptor(NEW.id);
+        END IF;
+    END IF;
+
+    RETURN NEW;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+ALTER TABLE authority.control_set_authority_field ADD COLUMN nfi CHAR(1);
+
+-- Entries that need to respect an NFI
+UPDATE authority.control_set_authority_field SET nfi = '2'
+    WHERE id IN (4,24,44,64);
+
+DROP TRIGGER authority_full_rec_fti_trigger ON authority.full_rec;
+CREATE TRIGGER authority_full_rec_fti_trigger
+    BEFORE UPDATE OR INSERT ON authority.full_rec
+    FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
+
+CREATE OR REPLACE FUNCTION authority.normalize_heading( marcxml TEXT, no_thesaurus BOOL ) RETURNS TEXT AS $func$
+DECLARE
+    acsaf           authority.control_set_authority_field%ROWTYPE;
+    tag_used        TEXT;
+    nfi_used        TEXT;
+    sf              TEXT;
+    thes_code       TEXT;
+    cset            INT;
+    heading_text    TEXT;
+    tmp_text        TEXT;
+    first_sf        BOOL;
+    auth_id         INT DEFAULT oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', marcxml)::INT;
+BEGIN
+    SELECT control_set INTO cset FROM authority.record_entry WHERE id = auth_id;
+
+    IF cset IS NULL THEN
+        SELECT  control_set INTO cset
+          FROM  authority.control_set_authority_field
+          WHERE tag IN ( SELECT  UNNEST(XPATH('//*[starts-with(@tag,"1")]/@tag',marcxml::XML)::TEXT[]))
+          LIMIT 1;
+    END IF;
+
+    IF thes_code = 'z' THEN
+        thes_code := COALESCE( oils_xpath_string('//*[@tag="040"]/*[@code="f"][1]', marcxml), '' );
+    END IF;
+
+    heading_text := '';
+    FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset AND main_entry IS NULL LOOP
+        tag_used := acsaf.tag;
+        nfi_used := acsaf.nfi;
+        first_sf := TRUE;
+        FOR sf IN SELECT * FROM regexp_split_to_table(acsaf.sf_list,'') LOOP
+            tmp_text := oils_xpath_string('//*[@tag="'||tag_used||'"]/*[@code="'||sf||'"]', marcxml);
+
+            IF first_sf AND tmp_text IS NOT NULL AND nfi_used IS NOT NULL THEN
+
+                tmp_text := SUBSTRING(
+                    tmp_text FROM
+                    COALESCE(
+                        NULLIF(
+                            REGEXP_REPLACE(
+                                oils_xpath_string('//*[@tag="'||tag_used||'"]/@ind'||nfi_used, marcxml),
+                                $$\D+$$,
+                                '',
+                                'g'
+                            ),
+                            ''
+                        )::INT,
+                        0
+                    ) + 1
+                );
+
+            END IF;
+
+            first_sf := FALSE;
+
+            IF tmp_text IS NOT NULL AND tmp_text <> '' THEN
+                heading_text := heading_text || E'\u2021' || sf || ' ' || tmp_text;
+            END IF;
+        END LOOP;
+        EXIT WHEN heading_text <> '';
+    END LOOP;
+
+    IF heading_text <> '' THEN
+        IF no_thesaurus IS TRUE THEN
+            heading_text := tag_used || ' ' || public.naco_normalize(heading_text);
+        ELSE
+            heading_text := tag_used || '_' || COALESCE(nfi_used,'-') || '_' || thes_code || ' ' || public.naco_normalize(heading_text);
+        END IF;
+    ELSE
+        heading_text := 'NOHEADING_' || thes_code || ' ' || MD5(marcxml);
+    END IF;
+
+    RETURN heading_text;
+END;
+$func$ LANGUAGE PLPGSQL IMMUTABLE;
+
+
+CREATE TABLE authority.simple_heading (
+    id              BIGSERIAL   PRIMARY KEY,
+    record          BIGINT      NOT NULL REFERENCES authority.record_entry (id),
+    atag            INT         NOT NULL REFERENCES authority.control_set_authority_field (id),
+    value           TEXT        NOT NULL,
+    sort_value      TEXT        NOT NULL,
+    index_vector    tsvector    NOT NULL
+);
+CREATE TRIGGER authority_simple_heading_fti_trigger
+    BEFORE UPDATE OR INSERT ON authority.simple_heading
+    FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
+
+CREATE INDEX authority_simple_heading_index_vector_idx ON authority.simple_heading USING GIST (index_vector);
+CREATE INDEX authority_simple_heading_value_idx ON authority.simple_heading (value);
+CREATE INDEX authority_simple_heading_sort_value_idx ON authority.simple_heading (sort_value);
+
+CREATE OR REPLACE FUNCTION authority.simple_heading_set( marcxml TEXT ) RETURNS SETOF authority.simple_heading AS $func$
+DECLARE
+    res             authority.simple_heading%ROWTYPE;
+    acsaf           authority.control_set_authority_field%ROWTYPE;
+    tag_used        TEXT;
+    nfi_used        TEXT;
+    sf              TEXT;
+    cset            INT;
+    heading_text    TEXT;
+    sort_text       TEXT;
+    tmp_text        TEXT;
+    tmp_xml         TEXT;
+    first_sf        BOOL;
+    auth_id         INT DEFAULT oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', marcxml)::INT;
+BEGIN
+
+    res.record := auth_id;
+
+    SELECT  control_set INTO cset
+      FROM  authority.control_set_authority_field
+      WHERE tag IN ( SELECT UNNEST(XPATH('//*[starts-with(@tag,"1")]/@tag',marcxml::XML)::TEXT[]) )
+      LIMIT 1;
+
+    FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset LOOP
+
+        res.atag := acsaf.id;
+        tag_used := acsaf.tag;
+        nfi_used := acsaf.nfi;
+
+        FOR tmp_xml IN SELECT UNNEST(XPATH('//*[@tag="'||tag_used||'"]', marcxml::XML)) LOOP
+            heading_text := '';
+
+            FOR sf IN SELECT * FROM regexp_split_to_table(acsaf.sf_list,'') LOOP
+                heading_text := heading_text || COALESCE( ' ' || oils_xpath_string('//*[@code="'||sf||'"]',tmp_xml::TEXT), '');
+            END LOOP;
+
+            heading_text := public.naco_normalize(heading_text);
+            
+            IF nfi_used IS NOT NULL THEN
+
+                sort_text := SUBSTRING(
+                    heading_text FROM
+                    COALESCE(
+                        NULLIF(
+                            REGEXP_REPLACE(
+                                oils_xpath_string('//*[@tag="'||tag_used||'"]/@ind'||nfi_used, marcxml),
+                                $$\D+$$,
+                                '',
+                                'g'
+                            ),
+                            ''
+                        )::INT,
+                        0
+                    ) + 1
+                );
+
+            ELSE
+                sort_text := heading_text;
+            END IF;
+
+            IF heading_text IS NOT NULL AND heading_text <> '' THEN
+                res.value := heading_text;
+                res.sort_value := sort_text;
+                RETURN NEXT res;
+            END IF;
+
+        END LOOP;
+
+    END LOOP;
+
+    RETURN;
+END;
+$func$ LANGUAGE PLPGSQL IMMUTABLE;
+
+-- Support function used to find the pivot for alpha-heading-browse style searching
+CREATE OR REPLACE FUNCTION authority.simple_heading_find_pivot( a INT[], q TEXT ) RETURNS TEXT AS $$
+DECLARE
+    sort_value_row  RECORD;
+    value_row       RECORD;
+    t_term          TEXT;
+BEGIN
+
+    t_term := public.naco_normalize(q);
+
+    SELECT  CASE WHEN ash.sort_value LIKE t_term || '%' THEN 1 ELSE 0 END
+                + CASE WHEN ash.value LIKE t_term || '%' THEN 1 ELSE 0 END AS rank,
+            ash.sort_value
+      INTO  sort_value_row
+      FROM  authority.simple_heading ash
+      WHERE ash.atag = ANY (a)
+            AND ash.sort_value >= t_term
+      ORDER BY rank DESC, ash.sort_value
+      LIMIT 1;
+
+    SELECT  CASE WHEN ash.sort_value LIKE t_term || '%' THEN 1 ELSE 0 END
+                + CASE WHEN ash.value LIKE t_term || '%' THEN 1 ELSE 0 END AS rank,
+            ash.sort_value
+      INTO  value_row
+      FROM  authority.simple_heading ash
+      WHERE ash.atag = ANY (a)
+            AND ash.value >= t_term
+      ORDER BY rank DESC, ash.sort_value
+      LIMIT 1;
+
+    IF value_row.rank > sort_value_row.rank THEN
+        RETURN value_row.sort_value;
+    ELSE
+        RETURN sort_value_row.sort_value;
+    END IF;
+END;
+$$ LANGUAGE PLPGSQL;
+
+
+CREATE OR REPLACE FUNCTION authority.simple_heading_browse_center( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
+DECLARE
+    pivot_sort_value    TEXT;
+    boffset             INT DEFAULT 0;
+    aoffset             INT DEFAULT 0;
+    blimit              INT DEFAULT 0;
+    alimit              INT DEFAULT 0;
+BEGIN
+
+    pivot_sort_value := authority.simple_heading_find_pivot(atag_list,q);
+
+    IF page = 0 THEN
+        blimit := pagesize / 2;
+        alimit := blimit;
+
+        IF pagesize % 2 <> 0 THEN
+            alimit := alimit + 1;
+        END IF;
+    ELSE
+        blimit := pagesize;
+        alimit := blimit;
+
+        boffset := pagesize / 2;
+        aoffset := boffset;
+
+        IF pagesize % 2 <> 0 THEN
+            boffset := boffset + 1;
+        END IF;
+    END IF;
+
+    IF page <= 0 THEN
+        RETURN QUERY
+            -- "bottom" half of the browse results
+            SELECT id FROM (
+                SELECT  ash.id,
+                        row_number() over ()
+                  FROM  authority.simple_heading ash
+                  WHERE ash.atag = ANY (atag_list)
+                        AND ash.sort_value < pivot_sort_value
+                  ORDER BY ash.sort_value DESC
+                  LIMIT blimit
+                  OFFSET ABS(page) * pagesize - boffset
+            ) x ORDER BY row_number DESC;
+    END IF;
+
+    IF page >= 0 THEN
+        RETURN QUERY
+            -- "bottom" half of the browse results
+            SELECT  ash.id
+              FROM  authority.simple_heading ash
+              WHERE ash.atag = ANY (atag_list)
+                    AND ash.sort_value >= pivot_sort_value
+              ORDER BY ash.sort_value
+              LIMIT alimit
+              OFFSET ABS(page) * pagesize - aoffset;
+    END IF;
+END;
+$$ LANGUAGE PLPGSQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.simple_heading_browse_top( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+DECLARE
+    pivot_sort_value    TEXT;
+BEGIN
+
+    pivot_sort_value := authority.simple_heading_find_pivot(atag_list,q);
+
+    IF page < 0 THEN
+        RETURN QUERY
+            -- "bottom" half of the browse results
+            SELECT id FROM (
+                SELECT  ash.id,
+                        row_number() over ()
+                  FROM  authority.simple_heading ash
+                  WHERE ash.atag = ANY (atag_list)
+                        AND ash.sort_value < pivot_sort_value
+                  ORDER BY ash.sort_value DESC
+                  LIMIT pagesize
+                  OFFSET (ABS(page) - 1) * pagesize
+            ) x ORDER BY row_number DESC;
+    END IF;
+
+    IF page >= 0 THEN
+        RETURN QUERY
+            -- "bottom" half of the browse results
+            SELECT  ash.id
+              FROM  authority.simple_heading ash
+              WHERE ash.atag = ANY (atag_list)
+                    AND ash.sort_value >= pivot_sort_value
+              ORDER BY ash.sort_value
+              LIMIT pagesize
+              OFFSET ABS(page) * pagesize ;
+    END IF;
+END;
+$$ LANGUAGE PLPGSQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.simple_heading_search_rank( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT  ash.id
+      FROM  authority.simple_heading ash,
+            public.naco_normalize($2) t(term),
+            plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
+      WHERE ash.atag = ANY ($1)
+            AND ash.index_vector @@ ptsq.term
+      ORDER BY ts_rank_cd(ash.index_vector,ptsq.term,14)::numeric
+                    + CASE WHEN ash.sort_value LIKE t.term || '%' THEN 2 ELSE 0 END
+                    + CASE WHEN ash.value LIKE t.term || '%' THEN 1 ELSE 0 END DESC
+      LIMIT $4
+      OFFSET $4 * $3;
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.simple_heading_search_heading( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT  ash.id
+      FROM  authority.simple_heading ash,
+            public.naco_normalize($2) t(term),
+            plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
+      WHERE ash.atag = ANY ($1)
+            AND ash.index_vector @@ ptsq.term
+      ORDER BY ash.sort_value
+      LIMIT $4
+      OFFSET $4 * $3;
+$$ LANGUAGE SQL ROWS 10;
+
+
+CREATE OR REPLACE FUNCTION authority.axis_authority_tags(a TEXT) RETURNS INT[] AS $$
+    SELECT ARRAY_ACCUM(field) FROM authority.browse_axis_authority_field_map WHERE axis = $1;
+$$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION authority.axis_authority_tags_refs(a TEXT) RETURNS INT[] AS $$
+    SELECT  ARRAY_CAT(
+                ARRAY[a.field],
+                (SELECT ARRAY_ACCUM(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.field)
+            )
+      FROM  authority.browse_axis_authority_field_map a
+      WHERE axis = $1
+$$ LANGUAGE SQL;
+
+
+
+CREATE OR REPLACE FUNCTION authority.btag_authority_tags(btag TEXT) RETURNS INT[] AS $$
+    SELECT ARRAY_ACCUM(authority_field) FROM authority.control_set_bib_field WHERE tag = $1
+$$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION authority.btag_authority_tags_refs(btag TEXT) RETURNS INT[] AS $$
+    SELECT  ARRAY_CAT(
+                ARRAY[a.authority_field],
+                (SELECT ARRAY_ACCUM(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.authority_field)
+            )
+      FROM  authority.control_set_bib_field a
+      WHERE a.tag = $1
+$$ LANGUAGE SQL;
+
+
+
+CREATE OR REPLACE FUNCTION authority.atag_authority_tags(atag TEXT) RETURNS INT[] AS $$
+    SELECT ARRAY_ACCUM(id) FROM authority.control_set_authority_field WHERE tag = $1
+$$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION authority.atag_authority_tags_refs(atag TEXT) RETURNS INT[] AS $$
+    SELECT  ARRAY_CAT(
+                ARRAY[a.id],
+                (SELECT ARRAY_ACCUM(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.id)
+            )
+      FROM  authority.control_set_authority_field a
+      WHERE a.tag = $1
+$$ LANGUAGE SQL;
+
+
+CREATE OR REPLACE FUNCTION authority.axis_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.axis_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.btag_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.atag_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.axis_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.btag_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.atag_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+
+CREATE OR REPLACE FUNCTION authority.axis_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.axis_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.btag_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.atag_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.axis_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.btag_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.atag_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+
+CREATE OR REPLACE FUNCTION authority.axis_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.axis_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.btag_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.atag_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.axis_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.btag_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.atag_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+
+CREATE OR REPLACE FUNCTION authority.axis_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.axis_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.btag_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.atag_authority_tags($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.axis_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.btag_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.atag_authority_tags_refs($1), $2, $3, $4)
+$$ LANGUAGE SQL ROWS 10;
+
+
+COMMIT;
+
diff --git a/Open-ILS/xul/staff_client/server/cat/marcedit.js b/Open-ILS/xul/staff_client/server/cat/marcedit.js
index b18c8a7..066a084 100644
--- a/Open-ILS/xul/staff_client/server/cat/marcedit.js
+++ b/Open-ILS/xul/staff_client/server/cat/marcedit.js
@@ -1551,6 +1551,13 @@ function applyAuthority ( target, ui_sf, e4x_sf, new_vals ) {
     }
 
     for (var i = 0; i < new_vals.length; i++) {
+
+        /* indicators for the authority datafield are carried over in the main entry linking subfield */
+        if (new_vals[i].getAttribute('subfield') == '0') {
+            field. at ind1 = new_vals[i].getAttribute('ind1');
+            field. at ind2 = new_vals[i].getAttribute('ind2');
+        }
+
         if (!new_vals[i].getAttribute('subfield')) continue;
 
         var val = new_vals[i].getAttribute('value');
@@ -1907,6 +1914,8 @@ function buildAuthorityPopupSelector (field, grid, auth_org, auth_id) {
                 { "label"    : '\u2021' + '0' + ' (' + auth_org + ')' + auth_id,
                   "subfield" : '0',
                   "tag"      : dojo.attr(field, 'tag'),
+                  "ind1"     : dojo.attr(field, 'ind1'),
+                  "ind2"     : dojo.attr(field, 'ind2'),
                   "value"    : '(' + auth_org + ')' + auth_id
                 }
             )

-----------------------------------------------------------------------

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |    1 +
 .../Application/Storage/Publisher/authority.pm     |   45 ++
 .../perlmods/lib/OpenILS/Application/SuperCat.pm   |   86 +++
 Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm  |   41 +-
 Open-ILS/src/sql/Pg/002.functions.config.sql       |   72 +++
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    2 +-
 Open-ILS/src/sql/Pg/011.schema.authority.sql       |  449 ++++++++++++++++-
 Open-ILS/src/sql/Pg/020.schema.functions.sql       |   72 ---
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |   11 +-
 Open-ILS/src/sql/Pg/999.functions.global.sql       |    7 +
 .../src/sql/Pg/upgrade/0640.schema-acs-nfi.sql     |  544 ++++++++++++++++++++
 Open-ILS/xul/staff_client/server/cat/marcedit.js   |    9 +
 12 files changed, 1239 insertions(+), 100 deletions(-)
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/0640.schema-acs-nfi.sql


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list