There is a basic mistake:
docs refer to "Indexing a JSON Object aka  %DynamicObject 
But in your example, you use %Stream.GlobalCharacter  which is a totally different object
The fact that it contains a JSON formatted string is not visible from the outside of the stream.
Instead of writing it to the Stream (which is an overkill anyhow) convert it and store it as JSON Object. 
That's what I had to do in my example

OK. I was blocked by other activities. But this is my solution:
Assumption1:    Property JSON As %Stream.GlobalCharacter;
Assumption2:    You know the properties you want to index, as with normal tables 
The idea: A calculated property is mainly used for building indices 
The solution: the Stream needs to be presented as %DynamicObject to get the value.
 And here is it:

Property JSON As %Stream.GlobalCharacter;

Property FirstName As %String [ Calculated, SqlComputed ,
         SqlComputeCode = { set {*}=..GetDyn({ID},"FirstName") } ];

Property LastName As %String [ Calculated, SqlComputed ,
         SqlComputeCode = { set {*}=..GetDyn({ID},"LastName") } ];

Index fn on FirstName;
Index ln on LastName;

ClassMethod GetDyn(ID As %Integer, item = "") As %String
{
   set JSON=..%OpenId(ID).JSON
   do JSON.Rewind()
   set st=JSON.Read(3000000)
   set dyno={}.%FromJSON(st)
   set rep=$Property(dyno,item)
   quit rep
}

There is room to improve the speed of the method.    
Also saving your keys in individual properties during data load could be a valid approach.
The principle is always the same. %Stream --> %DynamicObject --> extract keys by name

A  rather personal view:
The development of customized tags was an attempt to hide COS from "TAG-SHUFFLERS"  
as we named back at the start of this century WebPage coders. Neither JS nor CSS was there yet,
But something more  "modern" than the previous VB5 and VB6 tools were required.

ISC was never a leader in WebPages rather a follower. Also with ZEN and MOJO.
And CSP was definitely never a buying argument for Caché or IRIS.
It was never a core business. Rather a necessary requirement, demanding quite some effort.
Today nobody is forced to drop existing development.  
It is there and it will persist, and it's not hidden, but it will not be pushed.

A personals note.
The first CSP Training for customers was written by my friend Salva (@Jose-Tomas Salvador) 
And I did the translation of this training to German and English.( ~ 2 decades back)

What you experience is the effect of the Global Buffer Pool.
The rule is to overwrite the least used buffer if a new is required.
So the older the buffer the higher the chance to be overwritten and later reloaded.
Purging queries only affects code not data

Possible option:  increase  your buffer pool (double or triple size)
or try this approach: https://community.intersystems.com/post/global-buffer-questions
suggested by @Julius Kavay 

This total ODD !  But it works.
The list doesn't allow Carret ^ for the positive Globals
for globals to skip Carret ^ is required !!!


ENS>set list="Ens.*.GBL,'^Ens.Me*.GBL,'^Ens.C*.GBL"
ENS>ENS>set sc=$system.OBJ.Export(.list)
;; output skipped ;;
ENS>ZWRITE    ;processed Globals
list("^Ens.ActiveMessage.gbl")=""
list("^Ens.AppData.gbl")=""
list("^Ens.BP.ContextD.gbl")=""
list("^Ens.BP.ThreadD.gbl")=""
list("^Ens.BP.ThreadI.gbl")=""
list("^Ens.BusinessProcessD.gbl")=""
list("^Ens.BusinessProcessI.gbl")=""
list("^Ens.Debug.gbl")=""
list("^Ens.DocClassMap.gbl")=""
list("^Ens.JobStatus.gbl")=""
list("^Ens.Mirror.gbl")=""
list("^Ens.Queue.gbl")=""
list("^Ens.Rule.gbl")=""
list("^Ens.Util.IOLogD.gbl")=""
list("^Ens.Util.IOLogI.gbl")=""
list("^Ens.Util.LogD.gbl")=""
list("^Ens.Util.LogI.gbl")=""
list("^Ens.Util.ScheduleD.gbl")=""
sc=1
My interpretation:
First, a local array is collected, and then the negated subscripts are deleted (requiring now ^   !!)